@blackcode_sa/metaestetics-api 1.5.32 → 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.
- package/dist/admin/index.d.mts +337 -1
- package/dist/admin/index.d.ts +337 -1
- package/dist/admin/index.js +597 -14
- package/dist/admin/index.mjs +595 -14
- package/dist/backoffice/index.d.mts +2 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +234 -0
- package/src/admin/booking/booking.calculator.ts +686 -0
- package/src/admin/booking/booking.types.ts +56 -0
- package/src/admin/booking/index.ts +3 -0
- package/src/admin/index.ts +3 -0
- package/src/services/appointment/appointment.service.ts +603 -0
- package/src/services/appointment/index.ts +2 -0
- package/src/services/appointment/utils/appointment.utils.ts +590 -0
- package/src/types/appointment/index.ts +161 -0
- package/src/types/procedure/index.ts +2 -0
- package/src/validations/appointment.schema.ts +125 -0
package/dist/admin/index.js
CHANGED
|
@@ -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(
|
|
71
|
+
constructor(firestore8) {
|
|
70
72
|
this.expo = new import_expo_server_sdk.Expo();
|
|
71
|
-
this.db =
|
|
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(
|
|
259
|
-
this.db =
|
|
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(
|
|
734
|
-
this.db =
|
|
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(
|
|
1070
|
-
this.db =
|
|
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(
|
|
1455
|
-
this.db =
|
|
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(
|
|
1570
|
-
this.db =
|
|
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(
|
|
1754
|
-
super(
|
|
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,
|