@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.mjs
CHANGED
|
@@ -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(
|
|
23
|
+
constructor(firestore8) {
|
|
24
24
|
this.expo = new Expo();
|
|
25
|
-
this.db =
|
|
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(
|
|
213
|
-
this.db =
|
|
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(
|
|
688
|
-
this.db =
|
|
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(
|
|
1024
|
-
this.db =
|
|
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(
|
|
1409
|
-
this.db =
|
|
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(
|
|
1524
|
-
this.db =
|
|
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(
|
|
1708
|
-
super(
|
|
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;
|
package/dist/index.d.mts
CHANGED