@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.
- package/LICENSE.md +21 -0
- package/README.md +23 -2
- package/dist/admin/App-BrB9n7EO.mjs +371 -0
- package/dist/admin/App-ZlqOsILc.js +371 -0
- package/dist/admin/SettingsPage-LR6F2vyx.js +410 -0
- package/dist/admin/SettingsPage-Xas3gvn4.mjs +410 -0
- package/dist/admin/ar-9_2mxMp2.js +339 -0
- package/dist/admin/ar-CrM4yxqb.mjs +339 -0
- package/dist/admin/de-D3NbAP6M.mjs +339 -0
- package/dist/admin/de-DBaO-D7Z.js +339 -0
- package/dist/admin/en-Cu-rn_xM.mjs +339 -0
- package/dist/admin/en-DAoI7qkk.js +339 -0
- package/dist/admin/es-DUqsu82s.js +339 -0
- package/dist/admin/es-Y0zfya4P.mjs +339 -0
- package/dist/admin/fr-BkLta_o8.js +339 -0
- package/dist/admin/fr-BrzKRqet.mjs +339 -0
- package/dist/admin/getTranslation-CZAJyMSt.mjs +5 -0
- package/dist/admin/getTranslation-Cd0_4UtW.js +4 -0
- package/dist/admin/id-BZz53ViW.mjs +339 -0
- package/dist/admin/id-ByxSagzq.js +339 -0
- package/dist/admin/index.js +1497 -0
- package/dist/admin/index.mjs +1492 -0
- package/dist/admin/pt-BSCDUkdd.js +339 -0
- package/dist/admin/pt-DUfISuZf.mjs +339 -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 +3 -0
- package/dist/admin/src/components/bookings/BookingSidePanel.d.ts +10 -0
- package/dist/admin/src/components/bookings/BookingsGridCanvas.d.ts +20 -0
- package/dist/admin/src/components/bookings/QuickCreateBookingModal.d.ts +11 -0
- package/dist/admin/src/index.d.ts +24 -0
- package/dist/admin/src/pages/App.d.ts +3 -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/permissions.d.ts +29 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/translations/cmLabels.d.ts +1 -0
- package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/admin/tr-_nhc9hk6.mjs +339 -0
- package/dist/admin/tr-r17l5F2K.js +339 -0
- package/dist/admin/ur-8edBd0Qo.mjs +339 -0
- package/dist/admin/ur-Dw_TAOZ3.js +339 -0
- package/dist/admin/zh-CeR0swEm.mjs +339 -0
- package/dist/admin/zh-D0z5sxxN.js +339 -0
- package/dist/server/index.js +3547 -0
- package/dist/server/index.mjs +3545 -0
- package/dist/server/src/bootstrap.d.ts +6 -0
- package/dist/server/src/config/index.d.ts +6 -0
- package/dist/server/src/content-types/booking/schema.json.d.ts +117 -0
- package/dist/server/src/content-types/index.d.ts +265 -0
- package/dist/server/src/content-types/resource/schema.json.d.ts +147 -0
- package/dist/server/src/controllers/admin.d.ts +30 -0
- package/dist/server/src/controllers/content-api.d.ts +32 -0
- package/dist/server/src/controllers/index.d.ts +28 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/errors.d.ts +18 -0
- package/dist/server/src/index.d.ts +347 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +14 -0
- package/dist/server/src/routes/content-api.d.ts +9 -0
- package/dist/server/src/routes/index.d.ts +30 -0
- package/dist/server/src/services/availability.d.ts +8 -0
- package/dist/server/src/services/index.d.ts +12 -0
- package/dist/server/src/services/reservation.d.ts +6 -0
- package/dist/server/src/services/reservationHelpers.d.ts +41 -0
- package/dist/server/src/services/reservationHostCapacity.d.ts +16 -0
- package/dist/server/src/services/reservationMutations.d.ts +32 -0
- package/dist/server/src/services/reservationSources.d.ts +4 -0
- package/dist/server/src/services/reservationValidators.d.ts +27 -0
- package/dist/server/src/services/settings.d.ts +8 -0
- package/dist/server/src/types.d.ts +99 -0
- package/dist/shared/calendarSystems.d.ts +3 -0
- package/dist/shared/icalAndCsv.d.ts +16 -0
- package/dist/shared/index.d.ts +6 -0
- package/dist/shared/index.js +752 -0
- package/dist/shared/index.mjs +729 -0
- package/dist/shared/pluginId.d.ts +2 -0
- package/dist/shared/slotEngine.d.ts +19 -0
- package/dist/shared/slotTimezone.d.ts +45 -0
- package/dist/shared/types.d.ts +245 -0
- 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
|
+
};
|