@blackcode_sa/metaestetics-api 1.5.31 → 1.5.33

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