@blackcode_sa/metaestetics-api 1.5.32 → 1.5.34

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.
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BaseMailingService: () => BaseMailingService,
34
+ BookingAdmin: () => BookingAdmin,
34
35
  ClinicAggregationService: () => ClinicAggregationService,
35
36
  NOTIFICATIONS_COLLECTION: () => NOTIFICATIONS_COLLECTION,
36
37
  NotificationStatus: () => NotificationStatus,
@@ -66,9 +67,9 @@ var NotificationStatus = /* @__PURE__ */ ((NotificationStatus2) => {
66
67
  var admin = __toESM(require("firebase-admin"));
67
68
  var import_expo_server_sdk = require("expo-server-sdk");
68
69
  var NotificationsAdmin = class {
69
- constructor(firestore7) {
70
+ constructor(firestore8) {
70
71
  this.expo = new import_expo_server_sdk.Expo();
71
- this.db = firestore7 || admin.firestore();
72
+ this.db = firestore8 || admin.firestore();
72
73
  }
73
74
  /**
74
75
  * Dohvata notifikaciju po ID-u
@@ -255,8 +256,8 @@ var ClinicAggregationService = class {
255
256
  * Constructor for ClinicAggregationService.
256
257
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
257
258
  */
258
- constructor(firestore7) {
259
- this.db = firestore7 || admin2.firestore();
259
+ constructor(firestore8) {
260
+ this.db = firestore8 || admin2.firestore();
260
261
  }
261
262
  /**
262
263
  * Adds clinic information to a clinic group when a new clinic is created
@@ -730,8 +731,8 @@ var ClinicAggregationService = class {
730
731
  var admin3 = __toESM(require("firebase-admin"));
731
732
  var CALENDAR_SUBCOLLECTION_ID2 = "calendar";
732
733
  var PractitionerAggregationService = class {
733
- constructor(firestore7) {
734
- this.db = firestore7 || admin3.firestore();
734
+ constructor(firestore8) {
735
+ this.db = firestore8 || admin3.firestore();
735
736
  }
736
737
  /**
737
738
  * Adds practitioner information to a clinic when a new practitioner is created
@@ -1066,8 +1067,8 @@ var PractitionerAggregationService = class {
1066
1067
  var admin4 = __toESM(require("firebase-admin"));
1067
1068
  var CALENDAR_SUBCOLLECTION_ID3 = "calendar";
1068
1069
  var ProcedureAggregationService = class {
1069
- constructor(firestore7) {
1070
- this.db = firestore7 || admin4.firestore();
1070
+ constructor(firestore8) {
1071
+ this.db = firestore8 || admin4.firestore();
1071
1072
  }
1072
1073
  /**
1073
1074
  * Adds procedure information to a practitioner when a new procedure is created
@@ -1451,8 +1452,8 @@ var ProcedureAggregationService = class {
1451
1452
  var admin5 = __toESM(require("firebase-admin"));
1452
1453
  var CALENDAR_SUBCOLLECTION_ID4 = "calendar";
1453
1454
  var PatientAggregationService = class {
1454
- constructor(firestore7) {
1455
- this.db = firestore7 || admin5.firestore();
1455
+ constructor(firestore8) {
1456
+ this.db = firestore8 || admin5.firestore();
1456
1457
  }
1457
1458
  // --- Methods for Patient Creation --- >
1458
1459
  // No specific aggregations defined for patient creation in the plan.
@@ -1566,8 +1567,8 @@ var BaseMailingService = class {
1566
1567
  * @param firestore Firestore instance provided by the caller
1567
1568
  * @param mailgunClient Mailgun client instance provided by the caller
1568
1569
  */
1569
- constructor(firestore7, mailgunClient) {
1570
- this.db = firestore7;
1570
+ constructor(firestore8, mailgunClient) {
1571
+ this.db = firestore8;
1571
1572
  this.mailgunClient = mailgunClient;
1572
1573
  }
1573
1574
  /**
@@ -1750,8 +1751,8 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
1750
1751
  * @param firestore Firestore instance provided by the caller
1751
1752
  * @param mailgunClient Mailgun client instance provided by the caller
1752
1753
  */
1753
- constructor(firestore7, mailgunClient) {
1754
- super(firestore7, mailgunClient);
1754
+ constructor(firestore8, mailgunClient) {
1755
+ super(firestore8, mailgunClient);
1755
1756
  this.DEFAULT_REGISTRATION_URL = "https://app.medclinic.com/register";
1756
1757
  this.DEFAULT_SUBJECT = "You've Been Invited to Join as a Practitioner";
1757
1758
  this.DEFAULT_FROM_ADDRESS = "MedClinic <no-reply@your-domain.com>";
@@ -1889,6 +1890,587 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
1889
1890
  }
1890
1891
  };
1891
1892
 
1893
+ // src/admin/booking/booking.admin.ts
1894
+ var admin7 = __toESM(require("firebase-admin"));
1895
+
1896
+ // src/admin/booking/booking.calculator.ts
1897
+ var import_firestore = require("firebase/firestore");
1898
+ var BookingAvailabilityCalculator = class {
1899
+ /**
1900
+ * Calculate available booking slots based on the provided data
1901
+ *
1902
+ * @param request - The request containing all necessary data for calculation
1903
+ * @returns Response with available booking slots
1904
+ */
1905
+ static calculateSlots(request) {
1906
+ const {
1907
+ clinic,
1908
+ practitioner,
1909
+ procedure,
1910
+ timeframe,
1911
+ clinicCalendarEvents,
1912
+ practitionerCalendarEvents
1913
+ } = request;
1914
+ const schedulingIntervalMinutes = clinic.schedulingInterval || this.DEFAULT_INTERVAL_MINUTES;
1915
+ const procedureDurationMinutes = procedure.duration;
1916
+ console.log(
1917
+ `Calculating slots with interval: ${schedulingIntervalMinutes}min and procedure duration: ${procedureDurationMinutes}min`
1918
+ );
1919
+ let availableIntervals = [
1920
+ { start: timeframe.start, end: timeframe.end }
1921
+ ];
1922
+ availableIntervals = this.applyClinicWorkingHours(
1923
+ availableIntervals,
1924
+ clinic.workingHours,
1925
+ timeframe
1926
+ );
1927
+ availableIntervals = this.subtractBlockingEvents(
1928
+ availableIntervals,
1929
+ clinicCalendarEvents
1930
+ );
1931
+ availableIntervals = this.applyPractitionerWorkingHours(
1932
+ availableIntervals,
1933
+ practitioner,
1934
+ clinic.id,
1935
+ timeframe
1936
+ );
1937
+ availableIntervals = this.subtractPractitionerBusyTimes(
1938
+ availableIntervals,
1939
+ practitionerCalendarEvents
1940
+ );
1941
+ console.log(
1942
+ `After all filters, have ${availableIntervals.length} available intervals`
1943
+ );
1944
+ const availableSlots = this.generateAvailableSlots(
1945
+ availableIntervals,
1946
+ schedulingIntervalMinutes,
1947
+ procedureDurationMinutes
1948
+ );
1949
+ return { availableSlots };
1950
+ }
1951
+ /**
1952
+ * Apply clinic working hours to available intervals
1953
+ *
1954
+ * @param intervals - Current available intervals
1955
+ * @param workingHours - Clinic working hours
1956
+ * @param timeframe - Overall timeframe being considered
1957
+ * @returns Intervals filtered by clinic working hours
1958
+ */
1959
+ static applyClinicWorkingHours(intervals, workingHours, timeframe) {
1960
+ if (!intervals.length) return [];
1961
+ console.log(
1962
+ `Applying clinic working hours to ${intervals.length} intervals`
1963
+ );
1964
+ const workingIntervals = this.createWorkingHoursIntervals(
1965
+ workingHours,
1966
+ timeframe.start.toDate(),
1967
+ timeframe.end.toDate()
1968
+ );
1969
+ return this.intersectIntervals(intervals, workingIntervals);
1970
+ }
1971
+ /**
1972
+ * Create time intervals for working hours across multiple days
1973
+ *
1974
+ * @param workingHours - Working hours definition
1975
+ * @param startDate - Start date of the overall timeframe
1976
+ * @param endDate - End date of the overall timeframe
1977
+ * @returns Array of time intervals representing working hours
1978
+ */
1979
+ static createWorkingHoursIntervals(workingHours, startDate, endDate) {
1980
+ const workingIntervals = [];
1981
+ const currentDate = new Date(startDate);
1982
+ currentDate.setHours(0, 0, 0, 0);
1983
+ const dayNameToNumber = {
1984
+ sunday: 0,
1985
+ monday: 1,
1986
+ tuesday: 2,
1987
+ wednesday: 3,
1988
+ thursday: 4,
1989
+ friday: 5,
1990
+ saturday: 6
1991
+ };
1992
+ while (currentDate <= endDate) {
1993
+ const dayOfWeek = currentDate.getDay();
1994
+ const dayName = Object.keys(dayNameToNumber).find(
1995
+ (key) => dayNameToNumber[key] === dayOfWeek
1996
+ );
1997
+ if (dayName && workingHours[dayName]) {
1998
+ const daySchedule = workingHours[dayName];
1999
+ if (daySchedule) {
2000
+ const [openHours, openMinutes] = daySchedule.open.split(":").map(Number);
2001
+ const [closeHours, closeMinutes] = daySchedule.close.split(":").map(Number);
2002
+ const workStart = new Date(currentDate);
2003
+ workStart.setHours(openHours, openMinutes, 0, 0);
2004
+ const workEnd = new Date(currentDate);
2005
+ workEnd.setHours(closeHours, closeMinutes, 0, 0);
2006
+ if (workEnd > startDate && workStart < endDate) {
2007
+ const intervalStart = workStart < startDate ? startDate : workStart;
2008
+ const intervalEnd = workEnd > endDate ? endDate : workEnd;
2009
+ workingIntervals.push({
2010
+ start: import_firestore.Timestamp.fromDate(intervalStart),
2011
+ end: import_firestore.Timestamp.fromDate(intervalEnd)
2012
+ });
2013
+ if (daySchedule.breaks && daySchedule.breaks.length > 0) {
2014
+ for (const breakTime of daySchedule.breaks) {
2015
+ const [breakStartHours, breakStartMinutes] = breakTime.start.split(":").map(Number);
2016
+ const [breakEndHours, breakEndMinutes] = breakTime.end.split(":").map(Number);
2017
+ const breakStart = new Date(currentDate);
2018
+ breakStart.setHours(breakStartHours, breakStartMinutes, 0, 0);
2019
+ const breakEnd = new Date(currentDate);
2020
+ breakEnd.setHours(breakEndHours, breakEndMinutes, 0, 0);
2021
+ workingIntervals.splice(
2022
+ -1,
2023
+ 1,
2024
+ ...this.subtractInterval(
2025
+ workingIntervals[workingIntervals.length - 1],
2026
+ {
2027
+ start: import_firestore.Timestamp.fromDate(breakStart),
2028
+ end: import_firestore.Timestamp.fromDate(breakEnd)
2029
+ }
2030
+ )
2031
+ );
2032
+ }
2033
+ }
2034
+ }
2035
+ }
2036
+ }
2037
+ currentDate.setDate(currentDate.getDate() + 1);
2038
+ }
2039
+ return workingIntervals;
2040
+ }
2041
+ /**
2042
+ * Subtract blocking events from available intervals
2043
+ *
2044
+ * @param intervals - Current available intervals
2045
+ * @param events - Calendar events to subtract
2046
+ * @returns Available intervals after removing blocking events
2047
+ */
2048
+ static subtractBlockingEvents(intervals, events) {
2049
+ if (!intervals.length) return [];
2050
+ console.log(`Subtracting ${events.length} blocking events`);
2051
+ const blockingEvents = events.filter(
2052
+ (event) => event.eventType === "blocking" /* BLOCKING */ || event.eventType === "break" /* BREAK */ || event.eventType === "free_day" /* FREE_DAY */
2053
+ );
2054
+ let result = [...intervals];
2055
+ for (const event of blockingEvents) {
2056
+ const { start, end } = event.eventTime;
2057
+ const blockingInterval = { start, end };
2058
+ const newResult = [];
2059
+ for (const interval of result) {
2060
+ const remainingIntervals = this.subtractInterval(
2061
+ interval,
2062
+ blockingInterval
2063
+ );
2064
+ newResult.push(...remainingIntervals);
2065
+ }
2066
+ result = newResult;
2067
+ }
2068
+ return result;
2069
+ }
2070
+ /**
2071
+ * Apply practitioner's specific working hours for the given clinic
2072
+ *
2073
+ * @param intervals - Current available intervals
2074
+ * @param practitioner - Practitioner object
2075
+ * @param clinicId - ID of the clinic
2076
+ * @param timeframe - Overall timeframe being considered
2077
+ * @returns Intervals filtered by practitioner's working hours
2078
+ */
2079
+ static applyPractitionerWorkingHours(intervals, practitioner, clinicId, timeframe) {
2080
+ if (!intervals.length) return [];
2081
+ console.log(`Applying practitioner working hours for clinic ${clinicId}`);
2082
+ const clinicWorkingHours = practitioner.clinicWorkingHours.find(
2083
+ (hours) => hours.clinicId === clinicId && hours.isActive
2084
+ );
2085
+ if (!clinicWorkingHours) {
2086
+ console.log(
2087
+ `No working hours found for practitioner at clinic ${clinicId}`
2088
+ );
2089
+ return [];
2090
+ }
2091
+ const workingIntervals = this.createPractitionerWorkingHoursIntervals(
2092
+ clinicWorkingHours.workingHours,
2093
+ timeframe.start.toDate(),
2094
+ timeframe.end.toDate()
2095
+ );
2096
+ return this.intersectIntervals(intervals, workingIntervals);
2097
+ }
2098
+ /**
2099
+ * Create time intervals for practitioner's working hours across multiple days
2100
+ *
2101
+ * @param workingHours - Practitioner's working hours definition
2102
+ * @param startDate - Start date of the overall timeframe
2103
+ * @param endDate - End date of the overall timeframe
2104
+ * @returns Array of time intervals representing practitioner's working hours
2105
+ */
2106
+ static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate) {
2107
+ const workingIntervals = [];
2108
+ const currentDate = new Date(startDate);
2109
+ currentDate.setHours(0, 0, 0, 0);
2110
+ const dayNameToNumber = {
2111
+ sunday: 0,
2112
+ monday: 1,
2113
+ tuesday: 2,
2114
+ wednesday: 3,
2115
+ thursday: 4,
2116
+ friday: 5,
2117
+ saturday: 6
2118
+ };
2119
+ while (currentDate <= endDate) {
2120
+ const dayOfWeek = currentDate.getDay();
2121
+ const dayName = Object.keys(dayNameToNumber).find(
2122
+ (key) => dayNameToNumber[key] === dayOfWeek
2123
+ );
2124
+ if (dayName && workingHours[dayName]) {
2125
+ const daySchedule = workingHours[dayName];
2126
+ if (daySchedule) {
2127
+ const [startHours, startMinutes] = daySchedule.start.split(":").map(Number);
2128
+ const [endHours, endMinutes] = daySchedule.end.split(":").map(Number);
2129
+ const workStart = new Date(currentDate);
2130
+ workStart.setHours(startHours, startMinutes, 0, 0);
2131
+ const workEnd = new Date(currentDate);
2132
+ workEnd.setHours(endHours, endMinutes, 0, 0);
2133
+ if (workEnd > startDate && workStart < endDate) {
2134
+ const intervalStart = workStart < startDate ? startDate : workStart;
2135
+ const intervalEnd = workEnd > endDate ? endDate : workEnd;
2136
+ workingIntervals.push({
2137
+ start: import_firestore.Timestamp.fromDate(intervalStart),
2138
+ end: import_firestore.Timestamp.fromDate(intervalEnd)
2139
+ });
2140
+ }
2141
+ }
2142
+ }
2143
+ currentDate.setDate(currentDate.getDate() + 1);
2144
+ }
2145
+ return workingIntervals;
2146
+ }
2147
+ /**
2148
+ * Subtract practitioner's busy times from available intervals
2149
+ *
2150
+ * @param intervals - Current available intervals
2151
+ * @param events - Practitioner's calendar events
2152
+ * @returns Available intervals after removing busy times
2153
+ */
2154
+ static subtractPractitionerBusyTimes(intervals, events) {
2155
+ if (!intervals.length) return [];
2156
+ console.log(`Subtracting ${events.length} practitioner events`);
2157
+ const busyEvents = events.filter(
2158
+ (event) => (
2159
+ // Include all blocking events
2160
+ event.eventType === "blocking" /* BLOCKING */ || event.eventType === "break" /* BREAK */ || event.eventType === "free_day" /* FREE_DAY */ || // Include appointments that are pending, confirmed, or rescheduled
2161
+ event.eventType === "appointment" /* APPOINTMENT */ && (event.status === "pending" /* PENDING */ || event.status === "confirmed" /* CONFIRMED */ || event.status === "rescheduled" /* RESCHEDULED */)
2162
+ )
2163
+ );
2164
+ let result = [...intervals];
2165
+ for (const event of busyEvents) {
2166
+ const { start, end } = event.eventTime;
2167
+ const busyInterval = { start, end };
2168
+ const newResult = [];
2169
+ for (const interval of result) {
2170
+ const remainingIntervals = this.subtractInterval(
2171
+ interval,
2172
+ busyInterval
2173
+ );
2174
+ newResult.push(...remainingIntervals);
2175
+ }
2176
+ result = newResult;
2177
+ }
2178
+ return result;
2179
+ }
2180
+ /**
2181
+ * Generate available booking slots based on the final available intervals
2182
+ *
2183
+ * @param intervals - Final available intervals
2184
+ * @param intervalMinutes - Scheduling interval in minutes
2185
+ * @param durationMinutes - Procedure duration in minutes
2186
+ * @returns Array of available booking slots
2187
+ */
2188
+ static generateAvailableSlots(intervals, intervalMinutes, durationMinutes) {
2189
+ const slots = [];
2190
+ console.log(
2191
+ `Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure`
2192
+ );
2193
+ const durationMs = durationMinutes * 60 * 1e3;
2194
+ const intervalMs = intervalMinutes * 60 * 1e3;
2195
+ for (const interval of intervals) {
2196
+ const intervalStart = interval.start.toDate();
2197
+ const intervalEnd = interval.end.toDate();
2198
+ let slotStart = new Date(intervalStart);
2199
+ const minutesIntoDay = slotStart.getHours() * 60 + slotStart.getMinutes();
2200
+ const minutesRemainder = minutesIntoDay % intervalMinutes;
2201
+ if (minutesRemainder > 0) {
2202
+ slotStart.setMinutes(
2203
+ slotStart.getMinutes() + (intervalMinutes - minutesRemainder)
2204
+ );
2205
+ }
2206
+ while (slotStart.getTime() + durationMs <= intervalEnd.getTime()) {
2207
+ const slotEnd = new Date(slotStart.getTime() + durationMs);
2208
+ if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
2209
+ slots.push({
2210
+ start: import_firestore.Timestamp.fromDate(slotStart)
2211
+ });
2212
+ }
2213
+ slotStart = new Date(slotStart.getTime() + intervalMs);
2214
+ }
2215
+ }
2216
+ console.log(`Generated ${slots.length} available slots`);
2217
+ return slots;
2218
+ }
2219
+ /**
2220
+ * Check if a time slot is fully available within the given intervals
2221
+ *
2222
+ * @param slotStart - Start time of the slot
2223
+ * @param slotEnd - End time of the slot
2224
+ * @param intervals - Available intervals
2225
+ * @returns True if the slot is fully contained within an available interval
2226
+ */
2227
+ static isSlotFullyAvailable(slotStart, slotEnd, intervals) {
2228
+ return intervals.some((interval) => {
2229
+ const intervalStart = interval.start.toDate();
2230
+ const intervalEnd = interval.end.toDate();
2231
+ return slotStart >= intervalStart && slotEnd <= intervalEnd;
2232
+ });
2233
+ }
2234
+ /**
2235
+ * Intersect two sets of time intervals
2236
+ *
2237
+ * @param intervalsA - First set of intervals
2238
+ * @param intervalsB - Second set of intervals
2239
+ * @returns Intersection of the two sets of intervals
2240
+ */
2241
+ static intersectIntervals(intervalsA, intervalsB) {
2242
+ const result = [];
2243
+ for (const intervalA of intervalsA) {
2244
+ for (const intervalB of intervalsB) {
2245
+ const intersectionStart = intervalA.start.toMillis() > intervalB.start.toMillis() ? intervalA.start : intervalB.start;
2246
+ const intersectionEnd = intervalA.end.toMillis() < intervalB.end.toMillis() ? intervalA.end : intervalB.end;
2247
+ if (intersectionStart.toMillis() < intersectionEnd.toMillis()) {
2248
+ result.push({
2249
+ start: intersectionStart,
2250
+ end: intersectionEnd
2251
+ });
2252
+ }
2253
+ }
2254
+ }
2255
+ return this.mergeOverlappingIntervals(result);
2256
+ }
2257
+ /**
2258
+ * Subtract one interval from another, potentially resulting in 0, 1, or 2 intervals
2259
+ *
2260
+ * @param interval - Interval to subtract from
2261
+ * @param subtrahend - Interval to subtract
2262
+ * @returns Array of remaining intervals after subtraction
2263
+ */
2264
+ static subtractInterval(interval, subtrahend) {
2265
+ if (interval.end.toMillis() <= subtrahend.start.toMillis() || interval.start.toMillis() >= subtrahend.end.toMillis()) {
2266
+ return [interval];
2267
+ }
2268
+ if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() >= interval.end.toMillis()) {
2269
+ return [];
2270
+ }
2271
+ if (subtrahend.start.toMillis() > interval.start.toMillis() && subtrahend.end.toMillis() < interval.end.toMillis()) {
2272
+ return [
2273
+ {
2274
+ start: interval.start,
2275
+ end: subtrahend.start
2276
+ },
2277
+ {
2278
+ start: subtrahend.end,
2279
+ end: interval.end
2280
+ }
2281
+ ];
2282
+ }
2283
+ if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() > interval.start.toMillis()) {
2284
+ return [
2285
+ {
2286
+ start: subtrahend.end,
2287
+ end: interval.end
2288
+ }
2289
+ ];
2290
+ }
2291
+ return [
2292
+ {
2293
+ start: interval.start,
2294
+ end: subtrahend.start
2295
+ }
2296
+ ];
2297
+ }
2298
+ /**
2299
+ * Merge overlapping intervals to simplify the result
2300
+ *
2301
+ * @param intervals - Intervals to merge
2302
+ * @returns Merged intervals
2303
+ */
2304
+ static mergeOverlappingIntervals(intervals) {
2305
+ if (intervals.length <= 1) return intervals;
2306
+ const sorted = [...intervals].sort(
2307
+ (a, b) => a.start.toMillis() - b.start.toMillis()
2308
+ );
2309
+ const result = [sorted[0]];
2310
+ for (let i = 1; i < sorted.length; i++) {
2311
+ const current = sorted[i];
2312
+ const lastResult = result[result.length - 1];
2313
+ if (current.start.toMillis() <= lastResult.end.toMillis()) {
2314
+ if (current.end.toMillis() > lastResult.end.toMillis()) {
2315
+ lastResult.end = current.end;
2316
+ }
2317
+ } else {
2318
+ result.push(current);
2319
+ }
2320
+ }
2321
+ return result;
2322
+ }
2323
+ };
2324
+ /** Default scheduling interval in minutes if not specified by the clinic */
2325
+ BookingAvailabilityCalculator.DEFAULT_INTERVAL_MINUTES = 15;
2326
+
2327
+ // src/admin/booking/booking.admin.ts
2328
+ var BookingAdmin = class {
2329
+ /**
2330
+ * Creates a new BookingAdmin instance
2331
+ * @param firestore - Firestore instance provided by the caller
2332
+ */
2333
+ constructor(firestore8) {
2334
+ this.db = firestore8 || admin7.firestore();
2335
+ }
2336
+ /**
2337
+ * Gets available booking time slots for a specific clinic, practitioner, and procedure
2338
+ *
2339
+ * @param clinicId - ID of the clinic
2340
+ * @param practitionerId - ID of the practitioner
2341
+ * @param procedureId - ID of the procedure
2342
+ * @param timeframe - Time range to check for availability
2343
+ * @returns Promise resolving to an array of available booking slots
2344
+ */
2345
+ async getAvailableBookingSlots(clinicId, practitionerId, procedureId, timeframe) {
2346
+ try {
2347
+ console.log(
2348
+ `[BookingAdmin] Getting available slots for clinic ${clinicId}, practitioner ${practitionerId}, procedure ${procedureId}`
2349
+ );
2350
+ const start = timeframe.start instanceof Date ? admin7.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
2351
+ const end = timeframe.end instanceof Date ? admin7.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
2352
+ const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
2353
+ if (!clinicDoc.exists) {
2354
+ throw new Error(`Clinic ${clinicId} not found`);
2355
+ }
2356
+ const clinic = clinicDoc.data();
2357
+ const practitionerDoc = await this.db.collection("practitioners").doc(practitionerId).get();
2358
+ if (!practitionerDoc.exists) {
2359
+ throw new Error(`Practitioner ${practitionerId} not found`);
2360
+ }
2361
+ const practitioner = practitionerDoc.data();
2362
+ const procedureDoc = await this.db.collection("procedures").doc(procedureId).get();
2363
+ if (!procedureDoc.exists) {
2364
+ throw new Error(`Procedure ${procedureId} not found`);
2365
+ }
2366
+ const procedure = procedureDoc.data();
2367
+ const clinicCalendarEvents = await this.getClinicCalendarEvents(
2368
+ clinicId,
2369
+ start,
2370
+ end
2371
+ );
2372
+ const practitionerCalendarEvents = await this.getPractitionerCalendarEvents(practitionerId, start, end);
2373
+ const convertedTimeframe = {
2374
+ start: this.adminTimestampToClientTimestamp(start),
2375
+ end: this.adminTimestampToClientTimestamp(end)
2376
+ };
2377
+ const request = {
2378
+ clinic,
2379
+ practitioner,
2380
+ procedure,
2381
+ timeframe: convertedTimeframe,
2382
+ clinicCalendarEvents: this.convertEventsTimestamps(clinicCalendarEvents),
2383
+ practitionerCalendarEvents: this.convertEventsTimestamps(
2384
+ practitionerCalendarEvents
2385
+ )
2386
+ };
2387
+ const result = BookingAvailabilityCalculator.calculateSlots(request);
2388
+ return {
2389
+ availableSlots: result.availableSlots.map((slot) => ({
2390
+ start: admin7.firestore.Timestamp.fromMillis(slot.start.toMillis())
2391
+ }))
2392
+ };
2393
+ } catch (error) {
2394
+ console.error("[BookingAdmin] Error getting available slots:", error);
2395
+ throw error;
2396
+ }
2397
+ }
2398
+ /**
2399
+ * Converts an admin Firestore Timestamp to a client Firestore Timestamp
2400
+ */
2401
+ adminTimestampToClientTimestamp(timestamp) {
2402
+ return {
2403
+ seconds: timestamp.seconds,
2404
+ nanoseconds: timestamp.nanoseconds,
2405
+ toDate: () => timestamp.toDate(),
2406
+ toMillis: () => timestamp.toMillis(),
2407
+ valueOf: () => timestamp.valueOf()
2408
+ // Add any other required methods/properties
2409
+ };
2410
+ }
2411
+ /**
2412
+ * Converts timestamps in calendar events from admin Firestore Timestamps to client Firestore Timestamps
2413
+ */
2414
+ convertEventsTimestamps(events) {
2415
+ return events.map((event) => ({
2416
+ ...event,
2417
+ eventTime: {
2418
+ start: this.adminTimestampToClientTimestamp(event.eventTime.start),
2419
+ end: this.adminTimestampToClientTimestamp(event.eventTime.end)
2420
+ }
2421
+ // Convert any other timestamps in the event if needed
2422
+ }));
2423
+ }
2424
+ /**
2425
+ * Fetches clinic calendar events for a specific time range
2426
+ *
2427
+ * @param clinicId - ID of the clinic
2428
+ * @param start - Start time of the range
2429
+ * @param end - End time of the range
2430
+ * @returns Promise resolving to an array of calendar events
2431
+ */
2432
+ async getClinicCalendarEvents(clinicId, start, end) {
2433
+ try {
2434
+ const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
2435
+ const snapshot = await eventsRef.get();
2436
+ return snapshot.docs.map((doc) => ({
2437
+ ...doc.data(),
2438
+ id: doc.id
2439
+ }));
2440
+ } catch (error) {
2441
+ console.error(
2442
+ `[BookingAdmin] Error fetching clinic calendar events:`,
2443
+ error
2444
+ );
2445
+ return [];
2446
+ }
2447
+ }
2448
+ /**
2449
+ * Fetches practitioner calendar events for a specific time range
2450
+ *
2451
+ * @param practitionerId - ID of the practitioner
2452
+ * @param start - Start time of the range
2453
+ * @param end - End time of the range
2454
+ * @returns Promise resolving to an array of calendar events
2455
+ */
2456
+ async getPractitionerCalendarEvents(practitionerId, start, end) {
2457
+ try {
2458
+ const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
2459
+ const snapshot = await eventsRef.get();
2460
+ return snapshot.docs.map((doc) => ({
2461
+ ...doc.data(),
2462
+ id: doc.id
2463
+ }));
2464
+ } catch (error) {
2465
+ console.error(
2466
+ `[BookingAdmin] Error fetching practitioner calendar events:`,
2467
+ error
2468
+ );
2469
+ return [];
2470
+ }
2471
+ }
2472
+ };
2473
+
1892
2474
  // src/types/index.ts
1893
2475
  var UserRole = /* @__PURE__ */ ((UserRole2) => {
1894
2476
  UserRole2["PATIENT"] = "patient";
@@ -1903,6 +2485,7 @@ console.log("[Admin Module] Initialized and services exported.");
1903
2485
  // Annotate the CommonJS export names for ESM import in node:
1904
2486
  0 && (module.exports = {
1905
2487
  BaseMailingService,
2488
+ BookingAdmin,
1906
2489
  ClinicAggregationService,
1907
2490
  NOTIFICATIONS_COLLECTION,
1908
2491
  NotificationStatus,