@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.
- package/dist/admin/index.d.mts +226 -1
- package/dist/admin/index.d.ts +226 -1
- package/dist/admin/index.js +597 -14
- package/dist/admin/index.mjs +596 -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 +9 -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,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(
|
|
70
|
+
constructor(firestore8) {
|
|
70
71
|
this.expo = new import_expo_server_sdk.Expo();
|
|
71
|
-
this.db =
|
|
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(
|
|
259
|
-
this.db =
|
|
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(
|
|
734
|
-
this.db =
|
|
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(
|
|
1070
|
-
this.db =
|
|
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(
|
|
1455
|
-
this.db =
|
|
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(
|
|
1570
|
-
this.db =
|
|
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(
|
|
1754
|
-
super(
|
|
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,
|