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