@blackcode_sa/metaestetics-api 1.6.4 → 1.6.5
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 +211 -0
- package/dist/admin/index.d.ts +211 -0
- package/dist/admin/index.js +1234 -994
- package/dist/admin/index.mjs +1236 -996
- package/dist/backoffice/index.d.mts +2 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/index.d.mts +42 -75
- package/dist/index.d.ts +42 -75
- package/dist/index.js +74 -305
- package/dist/index.mjs +75 -306
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +321 -0
- package/src/admin/booking/booking.admin.ts +376 -3
- package/src/backoffice/types/product.types.ts +2 -0
- package/src/services/appointment/appointment.service.ts +89 -182
- package/src/services/procedure/procedure.service.ts +1 -0
- package/src/types/appointment/index.ts +2 -1
- package/src/types/procedure/index.ts +7 -0
- package/src/validations/appointment.schema.ts +1 -3
- package/src/validations/procedure.schema.ts +3 -0
package/dist/admin/index.js
CHANGED
|
@@ -18496,6 +18496,7 @@ var CLINICS_COLLECTION = "clinics";
|
|
|
18496
18496
|
|
|
18497
18497
|
// src/types/patient/index.ts
|
|
18498
18498
|
var PATIENTS_COLLECTION = "patients";
|
|
18499
|
+
var PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
|
|
18499
18500
|
|
|
18500
18501
|
// src/admin/aggregation/clinic/clinic.aggregation.service.ts
|
|
18501
18502
|
var CALENDAR_SUBCOLLECTION_ID = "calendar";
|
|
@@ -20364,838 +20365,257 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
|
|
|
20364
20365
|
// src/admin/booking/booking.admin.ts
|
|
20365
20366
|
var admin7 = __toESM(require("firebase-admin"));
|
|
20366
20367
|
|
|
20367
|
-
//
|
|
20368
|
-
var
|
|
20369
|
-
var BookingAvailabilityCalculator = class {
|
|
20368
|
+
// node_modules/@firebase/util/dist/node-esm/index.node.esm.js
|
|
20369
|
+
var CONSTANTS = {
|
|
20370
20370
|
/**
|
|
20371
|
-
*
|
|
20372
|
-
*
|
|
20373
|
-
* @param request - The request containing all necessary data for calculation
|
|
20374
|
-
* @returns Response with available booking slots
|
|
20371
|
+
* @define {boolean} Whether this is the client Node.js SDK.
|
|
20375
20372
|
*/
|
|
20376
|
-
|
|
20377
|
-
const {
|
|
20378
|
-
clinic,
|
|
20379
|
-
practitioner,
|
|
20380
|
-
procedure,
|
|
20381
|
-
timeframe,
|
|
20382
|
-
clinicCalendarEvents,
|
|
20383
|
-
practitionerCalendarEvents
|
|
20384
|
-
} = request;
|
|
20385
|
-
const schedulingIntervalMinutes = clinic.schedulingInterval || this.DEFAULT_INTERVAL_MINUTES;
|
|
20386
|
-
const procedureDurationMinutes = procedure.duration;
|
|
20387
|
-
console.log(
|
|
20388
|
-
`Calculating slots with interval: ${schedulingIntervalMinutes}min and procedure duration: ${procedureDurationMinutes}min`
|
|
20389
|
-
);
|
|
20390
|
-
let availableIntervals = [
|
|
20391
|
-
{ start: timeframe.start, end: timeframe.end }
|
|
20392
|
-
];
|
|
20393
|
-
availableIntervals = this.applyClinicWorkingHours(
|
|
20394
|
-
availableIntervals,
|
|
20395
|
-
clinic.workingHours,
|
|
20396
|
-
timeframe
|
|
20397
|
-
);
|
|
20398
|
-
availableIntervals = this.subtractBlockingEvents(
|
|
20399
|
-
availableIntervals,
|
|
20400
|
-
clinicCalendarEvents
|
|
20401
|
-
);
|
|
20402
|
-
availableIntervals = this.applyPractitionerWorkingHours(
|
|
20403
|
-
availableIntervals,
|
|
20404
|
-
practitioner,
|
|
20405
|
-
clinic.id,
|
|
20406
|
-
timeframe
|
|
20407
|
-
);
|
|
20408
|
-
availableIntervals = this.subtractPractitionerBusyTimes(
|
|
20409
|
-
availableIntervals,
|
|
20410
|
-
practitionerCalendarEvents
|
|
20411
|
-
);
|
|
20412
|
-
console.log(
|
|
20413
|
-
`After all filters, have ${availableIntervals.length} available intervals`
|
|
20414
|
-
);
|
|
20415
|
-
const availableSlots = this.generateAvailableSlots(
|
|
20416
|
-
availableIntervals,
|
|
20417
|
-
schedulingIntervalMinutes,
|
|
20418
|
-
procedureDurationMinutes
|
|
20419
|
-
);
|
|
20420
|
-
return { availableSlots };
|
|
20421
|
-
}
|
|
20373
|
+
NODE_CLIENT: false,
|
|
20422
20374
|
/**
|
|
20423
|
-
*
|
|
20424
|
-
*
|
|
20425
|
-
* @param intervals - Current available intervals
|
|
20426
|
-
* @param workingHours - Clinic working hours
|
|
20427
|
-
* @param timeframe - Overall timeframe being considered
|
|
20428
|
-
* @returns Intervals filtered by clinic working hours
|
|
20375
|
+
* @define {boolean} Whether this is the Admin Node.js SDK.
|
|
20429
20376
|
*/
|
|
20430
|
-
|
|
20431
|
-
if (!intervals.length) return [];
|
|
20432
|
-
console.log(
|
|
20433
|
-
`Applying clinic working hours to ${intervals.length} intervals`
|
|
20434
|
-
);
|
|
20435
|
-
const workingIntervals = this.createWorkingHoursIntervals(
|
|
20436
|
-
workingHours,
|
|
20437
|
-
timeframe.start.toDate(),
|
|
20438
|
-
timeframe.end.toDate()
|
|
20439
|
-
);
|
|
20440
|
-
return this.intersectIntervals(intervals, workingIntervals);
|
|
20441
|
-
}
|
|
20377
|
+
NODE_ADMIN: false,
|
|
20442
20378
|
/**
|
|
20443
|
-
*
|
|
20444
|
-
*
|
|
20445
|
-
* @param workingHours - Working hours definition
|
|
20446
|
-
* @param startDate - Start date of the overall timeframe
|
|
20447
|
-
* @param endDate - End date of the overall timeframe
|
|
20448
|
-
* @returns Array of time intervals representing working hours
|
|
20379
|
+
* Firebase SDK Version
|
|
20449
20380
|
*/
|
|
20450
|
-
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20461
|
-
|
|
20462
|
-
}
|
|
20463
|
-
|
|
20464
|
-
|
|
20465
|
-
|
|
20466
|
-
|
|
20467
|
-
|
|
20468
|
-
|
|
20469
|
-
|
|
20470
|
-
|
|
20471
|
-
|
|
20472
|
-
const [closeHours, closeMinutes] = daySchedule.close.split(":").map(Number);
|
|
20473
|
-
const workStart = new Date(currentDate);
|
|
20474
|
-
workStart.setHours(openHours, openMinutes, 0, 0);
|
|
20475
|
-
const workEnd = new Date(currentDate);
|
|
20476
|
-
workEnd.setHours(closeHours, closeMinutes, 0, 0);
|
|
20477
|
-
if (workEnd > startDate && workStart < endDate) {
|
|
20478
|
-
const intervalStart = workStart < startDate ? startDate : workStart;
|
|
20479
|
-
const intervalEnd = workEnd > endDate ? endDate : workEnd;
|
|
20480
|
-
workingIntervals.push({
|
|
20481
|
-
start: import_firestore.Timestamp.fromDate(intervalStart),
|
|
20482
|
-
end: import_firestore.Timestamp.fromDate(intervalEnd)
|
|
20483
|
-
});
|
|
20484
|
-
if (daySchedule.breaks && daySchedule.breaks.length > 0) {
|
|
20485
|
-
for (const breakTime of daySchedule.breaks) {
|
|
20486
|
-
const [breakStartHours, breakStartMinutes] = breakTime.start.split(":").map(Number);
|
|
20487
|
-
const [breakEndHours, breakEndMinutes] = breakTime.end.split(":").map(Number);
|
|
20488
|
-
const breakStart = new Date(currentDate);
|
|
20489
|
-
breakStart.setHours(breakStartHours, breakStartMinutes, 0, 0);
|
|
20490
|
-
const breakEnd = new Date(currentDate);
|
|
20491
|
-
breakEnd.setHours(breakEndHours, breakEndMinutes, 0, 0);
|
|
20492
|
-
workingIntervals.splice(
|
|
20493
|
-
-1,
|
|
20494
|
-
1,
|
|
20495
|
-
...this.subtractInterval(
|
|
20496
|
-
workingIntervals[workingIntervals.length - 1],
|
|
20497
|
-
{
|
|
20498
|
-
start: import_firestore.Timestamp.fromDate(breakStart),
|
|
20499
|
-
end: import_firestore.Timestamp.fromDate(breakEnd)
|
|
20500
|
-
}
|
|
20501
|
-
)
|
|
20502
|
-
);
|
|
20503
|
-
}
|
|
20504
|
-
}
|
|
20505
|
-
}
|
|
20506
|
-
}
|
|
20507
|
-
}
|
|
20508
|
-
currentDate.setDate(currentDate.getDate() + 1);
|
|
20381
|
+
SDK_VERSION: "${JSCORE_VERSION}"
|
|
20382
|
+
};
|
|
20383
|
+
var stringToByteArray$1 = function(str) {
|
|
20384
|
+
const out = [];
|
|
20385
|
+
let p = 0;
|
|
20386
|
+
for (let i = 0; i < str.length; i++) {
|
|
20387
|
+
let c = str.charCodeAt(i);
|
|
20388
|
+
if (c < 128) {
|
|
20389
|
+
out[p++] = c;
|
|
20390
|
+
} else if (c < 2048) {
|
|
20391
|
+
out[p++] = c >> 6 | 192;
|
|
20392
|
+
out[p++] = c & 63 | 128;
|
|
20393
|
+
} else if ((c & 64512) === 55296 && i + 1 < str.length && (str.charCodeAt(i + 1) & 64512) === 56320) {
|
|
20394
|
+
c = 65536 + ((c & 1023) << 10) + (str.charCodeAt(++i) & 1023);
|
|
20395
|
+
out[p++] = c >> 18 | 240;
|
|
20396
|
+
out[p++] = c >> 12 & 63 | 128;
|
|
20397
|
+
out[p++] = c >> 6 & 63 | 128;
|
|
20398
|
+
out[p++] = c & 63 | 128;
|
|
20399
|
+
} else {
|
|
20400
|
+
out[p++] = c >> 12 | 224;
|
|
20401
|
+
out[p++] = c >> 6 & 63 | 128;
|
|
20402
|
+
out[p++] = c & 63 | 128;
|
|
20509
20403
|
}
|
|
20510
|
-
return workingIntervals;
|
|
20511
20404
|
}
|
|
20512
|
-
|
|
20513
|
-
|
|
20514
|
-
|
|
20515
|
-
|
|
20516
|
-
|
|
20517
|
-
|
|
20518
|
-
|
|
20519
|
-
|
|
20520
|
-
|
|
20521
|
-
|
|
20522
|
-
|
|
20523
|
-
|
|
20524
|
-
)
|
|
20525
|
-
|
|
20526
|
-
|
|
20527
|
-
const
|
|
20528
|
-
const
|
|
20529
|
-
|
|
20530
|
-
|
|
20531
|
-
|
|
20532
|
-
|
|
20533
|
-
|
|
20534
|
-
|
|
20535
|
-
newResult.push(...remainingIntervals);
|
|
20536
|
-
}
|
|
20537
|
-
result = newResult;
|
|
20405
|
+
return out;
|
|
20406
|
+
};
|
|
20407
|
+
var byteArrayToString = function(bytes) {
|
|
20408
|
+
const out = [];
|
|
20409
|
+
let pos = 0, c = 0;
|
|
20410
|
+
while (pos < bytes.length) {
|
|
20411
|
+
const c1 = bytes[pos++];
|
|
20412
|
+
if (c1 < 128) {
|
|
20413
|
+
out[c++] = String.fromCharCode(c1);
|
|
20414
|
+
} else if (c1 > 191 && c1 < 224) {
|
|
20415
|
+
const c2 = bytes[pos++];
|
|
20416
|
+
out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
|
|
20417
|
+
} else if (c1 > 239 && c1 < 365) {
|
|
20418
|
+
const c2 = bytes[pos++];
|
|
20419
|
+
const c3 = bytes[pos++];
|
|
20420
|
+
const c4 = bytes[pos++];
|
|
20421
|
+
const u = ((c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63) - 65536;
|
|
20422
|
+
out[c++] = String.fromCharCode(55296 + (u >> 10));
|
|
20423
|
+
out[c++] = String.fromCharCode(56320 + (u & 1023));
|
|
20424
|
+
} else {
|
|
20425
|
+
const c2 = bytes[pos++];
|
|
20426
|
+
const c3 = bytes[pos++];
|
|
20427
|
+
out[c++] = String.fromCharCode((c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
|
|
20538
20428
|
}
|
|
20539
|
-
return result;
|
|
20540
20429
|
}
|
|
20430
|
+
return out.join("");
|
|
20431
|
+
};
|
|
20432
|
+
var base64 = {
|
|
20541
20433
|
/**
|
|
20542
|
-
*
|
|
20543
|
-
*
|
|
20544
|
-
* @param intervals - Current available intervals
|
|
20545
|
-
* @param practitioner - Practitioner object
|
|
20546
|
-
* @param clinicId - ID of the clinic
|
|
20547
|
-
* @param timeframe - Overall timeframe being considered
|
|
20548
|
-
* @returns Intervals filtered by practitioner's working hours
|
|
20434
|
+
* Maps bytes to characters.
|
|
20549
20435
|
*/
|
|
20550
|
-
|
|
20551
|
-
if (!intervals.length) return [];
|
|
20552
|
-
console.log(`Applying practitioner working hours for clinic ${clinicId}`);
|
|
20553
|
-
const clinicWorkingHours = practitioner.clinicWorkingHours.find(
|
|
20554
|
-
(hours) => hours.clinicId === clinicId && hours.isActive
|
|
20555
|
-
);
|
|
20556
|
-
if (!clinicWorkingHours) {
|
|
20557
|
-
console.log(
|
|
20558
|
-
`No working hours found for practitioner at clinic ${clinicId}`
|
|
20559
|
-
);
|
|
20560
|
-
return [];
|
|
20561
|
-
}
|
|
20562
|
-
const workingIntervals = this.createPractitionerWorkingHoursIntervals(
|
|
20563
|
-
clinicWorkingHours.workingHours,
|
|
20564
|
-
timeframe.start.toDate(),
|
|
20565
|
-
timeframe.end.toDate()
|
|
20566
|
-
);
|
|
20567
|
-
return this.intersectIntervals(intervals, workingIntervals);
|
|
20568
|
-
}
|
|
20436
|
+
byteToCharMap_: null,
|
|
20569
20437
|
/**
|
|
20570
|
-
*
|
|
20571
|
-
*
|
|
20572
|
-
* @param workingHours - Practitioner's working hours definition
|
|
20573
|
-
* @param startDate - Start date of the overall timeframe
|
|
20574
|
-
* @param endDate - End date of the overall timeframe
|
|
20575
|
-
* @returns Array of time intervals representing practitioner's working hours
|
|
20438
|
+
* Maps characters to bytes.
|
|
20576
20439
|
*/
|
|
20577
|
-
|
|
20578
|
-
const workingIntervals = [];
|
|
20579
|
-
const currentDate = new Date(startDate);
|
|
20580
|
-
currentDate.setHours(0, 0, 0, 0);
|
|
20581
|
-
const dayNameToNumber = {
|
|
20582
|
-
sunday: 0,
|
|
20583
|
-
monday: 1,
|
|
20584
|
-
tuesday: 2,
|
|
20585
|
-
wednesday: 3,
|
|
20586
|
-
thursday: 4,
|
|
20587
|
-
friday: 5,
|
|
20588
|
-
saturday: 6
|
|
20589
|
-
};
|
|
20590
|
-
while (currentDate <= endDate) {
|
|
20591
|
-
const dayOfWeek = currentDate.getDay();
|
|
20592
|
-
const dayName = Object.keys(dayNameToNumber).find(
|
|
20593
|
-
(key) => dayNameToNumber[key] === dayOfWeek
|
|
20594
|
-
);
|
|
20595
|
-
if (dayName && workingHours[dayName]) {
|
|
20596
|
-
const daySchedule = workingHours[dayName];
|
|
20597
|
-
if (daySchedule) {
|
|
20598
|
-
const [startHours, startMinutes] = daySchedule.start.split(":").map(Number);
|
|
20599
|
-
const [endHours, endMinutes] = daySchedule.end.split(":").map(Number);
|
|
20600
|
-
const workStart = new Date(currentDate);
|
|
20601
|
-
workStart.setHours(startHours, startMinutes, 0, 0);
|
|
20602
|
-
const workEnd = new Date(currentDate);
|
|
20603
|
-
workEnd.setHours(endHours, endMinutes, 0, 0);
|
|
20604
|
-
if (workEnd > startDate && workStart < endDate) {
|
|
20605
|
-
const intervalStart = workStart < startDate ? startDate : workStart;
|
|
20606
|
-
const intervalEnd = workEnd > endDate ? endDate : workEnd;
|
|
20607
|
-
workingIntervals.push({
|
|
20608
|
-
start: import_firestore.Timestamp.fromDate(intervalStart),
|
|
20609
|
-
end: import_firestore.Timestamp.fromDate(intervalEnd)
|
|
20610
|
-
});
|
|
20611
|
-
}
|
|
20612
|
-
}
|
|
20613
|
-
}
|
|
20614
|
-
currentDate.setDate(currentDate.getDate() + 1);
|
|
20615
|
-
}
|
|
20616
|
-
return workingIntervals;
|
|
20617
|
-
}
|
|
20440
|
+
charToByteMap_: null,
|
|
20618
20441
|
/**
|
|
20619
|
-
*
|
|
20620
|
-
*
|
|
20621
|
-
* @param intervals - Current available intervals
|
|
20622
|
-
* @param events - Practitioner's calendar events
|
|
20623
|
-
* @returns Available intervals after removing busy times
|
|
20442
|
+
* Maps bytes to websafe characters.
|
|
20443
|
+
* @private
|
|
20624
20444
|
*/
|
|
20625
|
-
|
|
20626
|
-
if (!intervals.length) return [];
|
|
20627
|
-
console.log(`Subtracting ${events.length} practitioner events`);
|
|
20628
|
-
const busyEvents = events.filter(
|
|
20629
|
-
(event) => (
|
|
20630
|
-
// Include all blocking events
|
|
20631
|
-
event.eventType === "blocking" /* BLOCKING */ || event.eventType === "break" /* BREAK */ || event.eventType === "free_day" /* FREE_DAY */ || // Include appointments that are pending, confirmed, or rescheduled
|
|
20632
|
-
event.eventType === "appointment" /* APPOINTMENT */ && (event.status === "pending" /* PENDING */ || event.status === "confirmed" /* CONFIRMED */ || event.status === "rescheduled" /* RESCHEDULED */)
|
|
20633
|
-
)
|
|
20634
|
-
);
|
|
20635
|
-
let result = [...intervals];
|
|
20636
|
-
for (const event of busyEvents) {
|
|
20637
|
-
const { start, end } = event.eventTime;
|
|
20638
|
-
const busyInterval = { start, end };
|
|
20639
|
-
const newResult = [];
|
|
20640
|
-
for (const interval of result) {
|
|
20641
|
-
const remainingIntervals = this.subtractInterval(
|
|
20642
|
-
interval,
|
|
20643
|
-
busyInterval
|
|
20644
|
-
);
|
|
20645
|
-
newResult.push(...remainingIntervals);
|
|
20646
|
-
}
|
|
20647
|
-
result = newResult;
|
|
20648
|
-
}
|
|
20649
|
-
return result;
|
|
20650
|
-
}
|
|
20445
|
+
byteToCharMapWebSafe_: null,
|
|
20651
20446
|
/**
|
|
20652
|
-
*
|
|
20653
|
-
*
|
|
20654
|
-
* @param intervals - Final available intervals
|
|
20655
|
-
* @param intervalMinutes - Scheduling interval in minutes
|
|
20656
|
-
* @param durationMinutes - Procedure duration in minutes
|
|
20657
|
-
* @returns Array of available booking slots
|
|
20447
|
+
* Maps websafe characters to bytes.
|
|
20448
|
+
* @private
|
|
20658
20449
|
*/
|
|
20659
|
-
|
|
20660
|
-
const slots = [];
|
|
20661
|
-
console.log(
|
|
20662
|
-
`Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure`
|
|
20663
|
-
);
|
|
20664
|
-
const durationMs = durationMinutes * 60 * 1e3;
|
|
20665
|
-
const intervalMs = intervalMinutes * 60 * 1e3;
|
|
20666
|
-
for (const interval of intervals) {
|
|
20667
|
-
const intervalStart = interval.start.toDate();
|
|
20668
|
-
const intervalEnd = interval.end.toDate();
|
|
20669
|
-
let slotStart = new Date(intervalStart);
|
|
20670
|
-
const minutesIntoDay = slotStart.getHours() * 60 + slotStart.getMinutes();
|
|
20671
|
-
const minutesRemainder = minutesIntoDay % intervalMinutes;
|
|
20672
|
-
if (minutesRemainder > 0) {
|
|
20673
|
-
slotStart.setMinutes(
|
|
20674
|
-
slotStart.getMinutes() + (intervalMinutes - minutesRemainder)
|
|
20675
|
-
);
|
|
20676
|
-
}
|
|
20677
|
-
while (slotStart.getTime() + durationMs <= intervalEnd.getTime()) {
|
|
20678
|
-
const slotEnd = new Date(slotStart.getTime() + durationMs);
|
|
20679
|
-
if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
|
|
20680
|
-
slots.push({
|
|
20681
|
-
start: import_firestore.Timestamp.fromDate(slotStart)
|
|
20682
|
-
});
|
|
20683
|
-
}
|
|
20684
|
-
slotStart = new Date(slotStart.getTime() + intervalMs);
|
|
20685
|
-
}
|
|
20686
|
-
}
|
|
20687
|
-
console.log(`Generated ${slots.length} available slots`);
|
|
20688
|
-
return slots;
|
|
20689
|
-
}
|
|
20450
|
+
charToByteMapWebSafe_: null,
|
|
20690
20451
|
/**
|
|
20691
|
-
*
|
|
20452
|
+
* Our default alphabet, shared between
|
|
20453
|
+
* ENCODED_VALS and ENCODED_VALS_WEBSAFE
|
|
20454
|
+
*/
|
|
20455
|
+
ENCODED_VALS_BASE: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
20456
|
+
/**
|
|
20457
|
+
* Our default alphabet. Value 64 (=) is special; it means "nothing."
|
|
20458
|
+
*/
|
|
20459
|
+
get ENCODED_VALS() {
|
|
20460
|
+
return this.ENCODED_VALS_BASE + "+/=";
|
|
20461
|
+
},
|
|
20462
|
+
/**
|
|
20463
|
+
* Our websafe alphabet.
|
|
20464
|
+
*/
|
|
20465
|
+
get ENCODED_VALS_WEBSAFE() {
|
|
20466
|
+
return this.ENCODED_VALS_BASE + "-_.";
|
|
20467
|
+
},
|
|
20468
|
+
/**
|
|
20469
|
+
* Whether this browser supports the atob and btoa functions. This extension
|
|
20470
|
+
* started at Mozilla but is now implemented by many browsers. We use the
|
|
20471
|
+
* ASSUME_* variables to avoid pulling in the full useragent detection library
|
|
20472
|
+
* but still allowing the standard per-browser compilations.
|
|
20692
20473
|
*
|
|
20693
|
-
* @param slotStart - Start time of the slot
|
|
20694
|
-
* @param slotEnd - End time of the slot
|
|
20695
|
-
* @param intervals - Available intervals
|
|
20696
|
-
* @returns True if the slot is fully contained within an available interval
|
|
20697
20474
|
*/
|
|
20698
|
-
|
|
20699
|
-
return intervals.some((interval) => {
|
|
20700
|
-
const intervalStart = interval.start.toDate();
|
|
20701
|
-
const intervalEnd = interval.end.toDate();
|
|
20702
|
-
return slotStart >= intervalStart && slotEnd <= intervalEnd;
|
|
20703
|
-
});
|
|
20704
|
-
}
|
|
20475
|
+
HAS_NATIVE_SUPPORT: typeof atob === "function",
|
|
20705
20476
|
/**
|
|
20706
|
-
*
|
|
20477
|
+
* Base64-encode an array of bytes.
|
|
20707
20478
|
*
|
|
20708
|
-
* @param
|
|
20709
|
-
*
|
|
20710
|
-
* @
|
|
20479
|
+
* @param input An array of bytes (numbers with
|
|
20480
|
+
* value in [0, 255]) to encode.
|
|
20481
|
+
* @param webSafe Boolean indicating we should use the
|
|
20482
|
+
* alternative alphabet.
|
|
20483
|
+
* @return The base64 encoded string.
|
|
20711
20484
|
*/
|
|
20712
|
-
|
|
20713
|
-
|
|
20714
|
-
|
|
20715
|
-
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
|
|
20721
|
-
|
|
20722
|
-
|
|
20485
|
+
encodeByteArray(input, webSafe) {
|
|
20486
|
+
if (!Array.isArray(input)) {
|
|
20487
|
+
throw Error("encodeByteArray takes an array as a parameter");
|
|
20488
|
+
}
|
|
20489
|
+
this.init_();
|
|
20490
|
+
const byteToCharMap = webSafe ? this.byteToCharMapWebSafe_ : this.byteToCharMap_;
|
|
20491
|
+
const output = [];
|
|
20492
|
+
for (let i = 0; i < input.length; i += 3) {
|
|
20493
|
+
const byte1 = input[i];
|
|
20494
|
+
const haveByte2 = i + 1 < input.length;
|
|
20495
|
+
const byte2 = haveByte2 ? input[i + 1] : 0;
|
|
20496
|
+
const haveByte3 = i + 2 < input.length;
|
|
20497
|
+
const byte3 = haveByte3 ? input[i + 2] : 0;
|
|
20498
|
+
const outByte1 = byte1 >> 2;
|
|
20499
|
+
const outByte2 = (byte1 & 3) << 4 | byte2 >> 4;
|
|
20500
|
+
let outByte3 = (byte2 & 15) << 2 | byte3 >> 6;
|
|
20501
|
+
let outByte4 = byte3 & 63;
|
|
20502
|
+
if (!haveByte3) {
|
|
20503
|
+
outByte4 = 64;
|
|
20504
|
+
if (!haveByte2) {
|
|
20505
|
+
outByte3 = 64;
|
|
20723
20506
|
}
|
|
20724
20507
|
}
|
|
20508
|
+
output.push(byteToCharMap[outByte1], byteToCharMap[outByte2], byteToCharMap[outByte3], byteToCharMap[outByte4]);
|
|
20725
20509
|
}
|
|
20726
|
-
return
|
|
20727
|
-
}
|
|
20510
|
+
return output.join("");
|
|
20511
|
+
},
|
|
20728
20512
|
/**
|
|
20729
|
-
*
|
|
20513
|
+
* Base64-encode a string.
|
|
20730
20514
|
*
|
|
20731
|
-
* @param
|
|
20732
|
-
* @param
|
|
20733
|
-
*
|
|
20515
|
+
* @param input A string to encode.
|
|
20516
|
+
* @param webSafe If true, we should use the
|
|
20517
|
+
* alternative alphabet.
|
|
20518
|
+
* @return The base64 encoded string.
|
|
20734
20519
|
*/
|
|
20735
|
-
|
|
20736
|
-
if (
|
|
20737
|
-
return
|
|
20738
|
-
}
|
|
20739
|
-
if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() >= interval.end.toMillis()) {
|
|
20740
|
-
return [];
|
|
20741
|
-
}
|
|
20742
|
-
if (subtrahend.start.toMillis() > interval.start.toMillis() && subtrahend.end.toMillis() < interval.end.toMillis()) {
|
|
20743
|
-
return [
|
|
20744
|
-
{
|
|
20745
|
-
start: interval.start,
|
|
20746
|
-
end: subtrahend.start
|
|
20747
|
-
},
|
|
20748
|
-
{
|
|
20749
|
-
start: subtrahend.end,
|
|
20750
|
-
end: interval.end
|
|
20751
|
-
}
|
|
20752
|
-
];
|
|
20753
|
-
}
|
|
20754
|
-
if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() > interval.start.toMillis()) {
|
|
20755
|
-
return [
|
|
20756
|
-
{
|
|
20757
|
-
start: subtrahend.end,
|
|
20758
|
-
end: interval.end
|
|
20759
|
-
}
|
|
20760
|
-
];
|
|
20520
|
+
encodeString(input, webSafe) {
|
|
20521
|
+
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
|
|
20522
|
+
return btoa(input);
|
|
20761
20523
|
}
|
|
20762
|
-
return
|
|
20763
|
-
|
|
20764
|
-
start: interval.start,
|
|
20765
|
-
end: subtrahend.start
|
|
20766
|
-
}
|
|
20767
|
-
];
|
|
20768
|
-
}
|
|
20524
|
+
return this.encodeByteArray(stringToByteArray$1(input), webSafe);
|
|
20525
|
+
},
|
|
20769
20526
|
/**
|
|
20770
|
-
*
|
|
20527
|
+
* Base64-decode a string.
|
|
20771
20528
|
*
|
|
20772
|
-
* @param
|
|
20773
|
-
* @
|
|
20529
|
+
* @param input to decode.
|
|
20530
|
+
* @param webSafe True if we should use the
|
|
20531
|
+
* alternative alphabet.
|
|
20532
|
+
* @return string representing the decoded value.
|
|
20774
20533
|
*/
|
|
20775
|
-
|
|
20776
|
-
if (
|
|
20777
|
-
|
|
20778
|
-
(a, b) => a.start.toMillis() - b.start.toMillis()
|
|
20779
|
-
);
|
|
20780
|
-
const result = [sorted[0]];
|
|
20781
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
20782
|
-
const current = sorted[i];
|
|
20783
|
-
const lastResult = result[result.length - 1];
|
|
20784
|
-
if (current.start.toMillis() <= lastResult.end.toMillis()) {
|
|
20785
|
-
if (current.end.toMillis() > lastResult.end.toMillis()) {
|
|
20786
|
-
lastResult.end = current.end;
|
|
20787
|
-
}
|
|
20788
|
-
} else {
|
|
20789
|
-
result.push(current);
|
|
20790
|
-
}
|
|
20534
|
+
decodeString(input, webSafe) {
|
|
20535
|
+
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
|
|
20536
|
+
return atob(input);
|
|
20791
20537
|
}
|
|
20792
|
-
return
|
|
20793
|
-
}
|
|
20794
|
-
};
|
|
20795
|
-
/** Default scheduling interval in minutes if not specified by the clinic */
|
|
20796
|
-
BookingAvailabilityCalculator.DEFAULT_INTERVAL_MINUTES = 15;
|
|
20797
|
-
|
|
20798
|
-
// src/admin/booking/booking.admin.ts
|
|
20799
|
-
var BookingAdmin = class {
|
|
20800
|
-
/**
|
|
20801
|
-
* Creates a new BookingAdmin instance
|
|
20802
|
-
* @param firestore - Firestore instance provided by the caller
|
|
20803
|
-
*/
|
|
20804
|
-
constructor(firestore9) {
|
|
20805
|
-
this.db = firestore9 || admin7.firestore();
|
|
20806
|
-
}
|
|
20538
|
+
return byteArrayToString(this.decodeStringToByteArray(input, webSafe));
|
|
20539
|
+
},
|
|
20807
20540
|
/**
|
|
20808
|
-
*
|
|
20541
|
+
* Base64-decode a string.
|
|
20809
20542
|
*
|
|
20810
|
-
*
|
|
20811
|
-
*
|
|
20812
|
-
*
|
|
20813
|
-
*
|
|
20814
|
-
*
|
|
20543
|
+
* In base-64 decoding, groups of four characters are converted into three
|
|
20544
|
+
* bytes. If the encoder did not apply padding, the input length may not
|
|
20545
|
+
* be a multiple of 4.
|
|
20546
|
+
*
|
|
20547
|
+
* In this case, the last group will have fewer than 4 characters, and
|
|
20548
|
+
* padding will be inferred. If the group has one or two characters, it decodes
|
|
20549
|
+
* to one byte. If the group has three characters, it decodes to two bytes.
|
|
20550
|
+
*
|
|
20551
|
+
* @param input Input to decode.
|
|
20552
|
+
* @param webSafe True if we should use the web-safe alphabet.
|
|
20553
|
+
* @return bytes representing the decoded value.
|
|
20815
20554
|
*/
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
20820
|
-
|
|
20821
|
-
const
|
|
20822
|
-
const
|
|
20823
|
-
const
|
|
20824
|
-
|
|
20825
|
-
|
|
20826
|
-
|
|
20827
|
-
|
|
20828
|
-
const
|
|
20829
|
-
|
|
20830
|
-
|
|
20555
|
+
decodeStringToByteArray(input, webSafe) {
|
|
20556
|
+
this.init_();
|
|
20557
|
+
const charToByteMap = webSafe ? this.charToByteMapWebSafe_ : this.charToByteMap_;
|
|
20558
|
+
const output = [];
|
|
20559
|
+
for (let i = 0; i < input.length; ) {
|
|
20560
|
+
const byte1 = charToByteMap[input.charAt(i++)];
|
|
20561
|
+
const haveByte2 = i < input.length;
|
|
20562
|
+
const byte2 = haveByte2 ? charToByteMap[input.charAt(i)] : 0;
|
|
20563
|
+
++i;
|
|
20564
|
+
const haveByte3 = i < input.length;
|
|
20565
|
+
const byte3 = haveByte3 ? charToByteMap[input.charAt(i)] : 64;
|
|
20566
|
+
++i;
|
|
20567
|
+
const haveByte4 = i < input.length;
|
|
20568
|
+
const byte4 = haveByte4 ? charToByteMap[input.charAt(i)] : 64;
|
|
20569
|
+
++i;
|
|
20570
|
+
if (byte1 == null || byte2 == null || byte3 == null || byte4 == null) {
|
|
20571
|
+
throw new DecodeBase64StringError();
|
|
20831
20572
|
}
|
|
20832
|
-
const
|
|
20833
|
-
|
|
20834
|
-
if (
|
|
20835
|
-
|
|
20573
|
+
const outByte1 = byte1 << 2 | byte2 >> 4;
|
|
20574
|
+
output.push(outByte1);
|
|
20575
|
+
if (byte3 !== 64) {
|
|
20576
|
+
const outByte2 = byte2 << 4 & 240 | byte3 >> 2;
|
|
20577
|
+
output.push(outByte2);
|
|
20578
|
+
if (byte4 !== 64) {
|
|
20579
|
+
const outByte3 = byte3 << 6 & 192 | byte4;
|
|
20580
|
+
output.push(outByte3);
|
|
20581
|
+
}
|
|
20836
20582
|
}
|
|
20837
|
-
const procedure = procedureDoc.data();
|
|
20838
|
-
const clinicCalendarEvents = await this.getClinicCalendarEvents(
|
|
20839
|
-
clinicId,
|
|
20840
|
-
start,
|
|
20841
|
-
end
|
|
20842
|
-
);
|
|
20843
|
-
const practitionerCalendarEvents = await this.getPractitionerCalendarEvents(practitionerId, start, end);
|
|
20844
|
-
const convertedTimeframe = {
|
|
20845
|
-
start: this.adminTimestampToClientTimestamp(start),
|
|
20846
|
-
end: this.adminTimestampToClientTimestamp(end)
|
|
20847
|
-
};
|
|
20848
|
-
const request = {
|
|
20849
|
-
clinic,
|
|
20850
|
-
practitioner,
|
|
20851
|
-
procedure,
|
|
20852
|
-
timeframe: convertedTimeframe,
|
|
20853
|
-
clinicCalendarEvents: this.convertEventsTimestamps(clinicCalendarEvents),
|
|
20854
|
-
practitionerCalendarEvents: this.convertEventsTimestamps(
|
|
20855
|
-
practitionerCalendarEvents
|
|
20856
|
-
)
|
|
20857
|
-
};
|
|
20858
|
-
const result = BookingAvailabilityCalculator.calculateSlots(request);
|
|
20859
|
-
return {
|
|
20860
|
-
availableSlots: result.availableSlots.map((slot) => ({
|
|
20861
|
-
start: admin7.firestore.Timestamp.fromMillis(slot.start.toMillis())
|
|
20862
|
-
}))
|
|
20863
|
-
};
|
|
20864
|
-
} catch (error) {
|
|
20865
|
-
console.error("[BookingAdmin] Error getting available slots:", error);
|
|
20866
|
-
throw error;
|
|
20867
20583
|
}
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
* Converts an admin Firestore Timestamp to a client Firestore Timestamp
|
|
20871
|
-
*/
|
|
20872
|
-
adminTimestampToClientTimestamp(timestamp) {
|
|
20873
|
-
return {
|
|
20874
|
-
seconds: timestamp.seconds,
|
|
20875
|
-
nanoseconds: timestamp.nanoseconds,
|
|
20876
|
-
toDate: () => timestamp.toDate(),
|
|
20877
|
-
toMillis: () => timestamp.toMillis(),
|
|
20878
|
-
valueOf: () => timestamp.valueOf()
|
|
20879
|
-
// Add any other required methods/properties
|
|
20880
|
-
};
|
|
20881
|
-
}
|
|
20584
|
+
return output;
|
|
20585
|
+
},
|
|
20882
20586
|
/**
|
|
20883
|
-
*
|
|
20587
|
+
* Lazy static initialization function. Called before
|
|
20588
|
+
* accessing any of the static map variables.
|
|
20589
|
+
* @private
|
|
20884
20590
|
*/
|
|
20885
|
-
|
|
20886
|
-
|
|
20887
|
-
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20591
|
+
init_() {
|
|
20592
|
+
if (!this.byteToCharMap_) {
|
|
20593
|
+
this.byteToCharMap_ = {};
|
|
20594
|
+
this.charToByteMap_ = {};
|
|
20595
|
+
this.byteToCharMapWebSafe_ = {};
|
|
20596
|
+
this.charToByteMapWebSafe_ = {};
|
|
20597
|
+
for (let i = 0; i < this.ENCODED_VALS.length; i++) {
|
|
20598
|
+
this.byteToCharMap_[i] = this.ENCODED_VALS.charAt(i);
|
|
20599
|
+
this.charToByteMap_[this.byteToCharMap_[i]] = i;
|
|
20600
|
+
this.byteToCharMapWebSafe_[i] = this.ENCODED_VALS_WEBSAFE.charAt(i);
|
|
20601
|
+
this.charToByteMapWebSafe_[this.byteToCharMapWebSafe_[i]] = i;
|
|
20602
|
+
if (i >= this.ENCODED_VALS_BASE.length) {
|
|
20603
|
+
this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(i)] = i;
|
|
20604
|
+
this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(i)] = i;
|
|
20605
|
+
}
|
|
20891
20606
|
}
|
|
20892
|
-
// Convert any other timestamps in the event if needed
|
|
20893
|
-
}));
|
|
20894
|
-
}
|
|
20895
|
-
/**
|
|
20896
|
-
* Fetches clinic calendar events for a specific time range
|
|
20897
|
-
*
|
|
20898
|
-
* @param clinicId - ID of the clinic
|
|
20899
|
-
* @param start - Start time of the range
|
|
20900
|
-
* @param end - End time of the range
|
|
20901
|
-
* @returns Promise resolving to an array of calendar events
|
|
20902
|
-
*/
|
|
20903
|
-
async getClinicCalendarEvents(clinicId, start, end) {
|
|
20904
|
-
try {
|
|
20905
|
-
const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
|
|
20906
|
-
const snapshot = await eventsRef.get();
|
|
20907
|
-
return snapshot.docs.map((doc) => ({
|
|
20908
|
-
...doc.data(),
|
|
20909
|
-
id: doc.id
|
|
20910
|
-
}));
|
|
20911
|
-
} catch (error) {
|
|
20912
|
-
console.error(
|
|
20913
|
-
`[BookingAdmin] Error fetching clinic calendar events:`,
|
|
20914
|
-
error
|
|
20915
|
-
);
|
|
20916
|
-
return [];
|
|
20917
20607
|
}
|
|
20918
20608
|
}
|
|
20919
|
-
|
|
20920
|
-
|
|
20921
|
-
|
|
20922
|
-
|
|
20923
|
-
|
|
20924
|
-
* @param end - End time of the range
|
|
20925
|
-
* @returns Promise resolving to an array of calendar events
|
|
20926
|
-
*/
|
|
20927
|
-
async getPractitionerCalendarEvents(practitionerId, start, end) {
|
|
20928
|
-
try {
|
|
20929
|
-
const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
|
|
20930
|
-
const snapshot = await eventsRef.get();
|
|
20931
|
-
return snapshot.docs.map((doc) => ({
|
|
20932
|
-
...doc.data(),
|
|
20933
|
-
id: doc.id
|
|
20934
|
-
}));
|
|
20935
|
-
} catch (error) {
|
|
20936
|
-
console.error(
|
|
20937
|
-
`[BookingAdmin] Error fetching practitioner calendar events:`,
|
|
20938
|
-
error
|
|
20939
|
-
);
|
|
20940
|
-
return [];
|
|
20941
|
-
}
|
|
20609
|
+
};
|
|
20610
|
+
var DecodeBase64StringError = class extends Error {
|
|
20611
|
+
constructor() {
|
|
20612
|
+
super(...arguments);
|
|
20613
|
+
this.name = "DecodeBase64StringError";
|
|
20942
20614
|
}
|
|
20943
20615
|
};
|
|
20944
|
-
|
|
20945
|
-
|
|
20946
|
-
|
|
20947
|
-
|
|
20948
|
-
// node_modules/@firebase/util/dist/node-esm/index.node.esm.js
|
|
20949
|
-
var CONSTANTS = {
|
|
20950
|
-
/**
|
|
20951
|
-
* @define {boolean} Whether this is the client Node.js SDK.
|
|
20952
|
-
*/
|
|
20953
|
-
NODE_CLIENT: false,
|
|
20954
|
-
/**
|
|
20955
|
-
* @define {boolean} Whether this is the Admin Node.js SDK.
|
|
20956
|
-
*/
|
|
20957
|
-
NODE_ADMIN: false,
|
|
20958
|
-
/**
|
|
20959
|
-
* Firebase SDK Version
|
|
20960
|
-
*/
|
|
20961
|
-
SDK_VERSION: "${JSCORE_VERSION}"
|
|
20962
|
-
};
|
|
20963
|
-
var stringToByteArray$1 = function(str) {
|
|
20964
|
-
const out = [];
|
|
20965
|
-
let p = 0;
|
|
20966
|
-
for (let i = 0; i < str.length; i++) {
|
|
20967
|
-
let c = str.charCodeAt(i);
|
|
20968
|
-
if (c < 128) {
|
|
20969
|
-
out[p++] = c;
|
|
20970
|
-
} else if (c < 2048) {
|
|
20971
|
-
out[p++] = c >> 6 | 192;
|
|
20972
|
-
out[p++] = c & 63 | 128;
|
|
20973
|
-
} else if ((c & 64512) === 55296 && i + 1 < str.length && (str.charCodeAt(i + 1) & 64512) === 56320) {
|
|
20974
|
-
c = 65536 + ((c & 1023) << 10) + (str.charCodeAt(++i) & 1023);
|
|
20975
|
-
out[p++] = c >> 18 | 240;
|
|
20976
|
-
out[p++] = c >> 12 & 63 | 128;
|
|
20977
|
-
out[p++] = c >> 6 & 63 | 128;
|
|
20978
|
-
out[p++] = c & 63 | 128;
|
|
20979
|
-
} else {
|
|
20980
|
-
out[p++] = c >> 12 | 224;
|
|
20981
|
-
out[p++] = c >> 6 & 63 | 128;
|
|
20982
|
-
out[p++] = c & 63 | 128;
|
|
20983
|
-
}
|
|
20984
|
-
}
|
|
20985
|
-
return out;
|
|
20986
|
-
};
|
|
20987
|
-
var byteArrayToString = function(bytes) {
|
|
20988
|
-
const out = [];
|
|
20989
|
-
let pos = 0, c = 0;
|
|
20990
|
-
while (pos < bytes.length) {
|
|
20991
|
-
const c1 = bytes[pos++];
|
|
20992
|
-
if (c1 < 128) {
|
|
20993
|
-
out[c++] = String.fromCharCode(c1);
|
|
20994
|
-
} else if (c1 > 191 && c1 < 224) {
|
|
20995
|
-
const c2 = bytes[pos++];
|
|
20996
|
-
out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
|
|
20997
|
-
} else if (c1 > 239 && c1 < 365) {
|
|
20998
|
-
const c2 = bytes[pos++];
|
|
20999
|
-
const c3 = bytes[pos++];
|
|
21000
|
-
const c4 = bytes[pos++];
|
|
21001
|
-
const u = ((c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63) - 65536;
|
|
21002
|
-
out[c++] = String.fromCharCode(55296 + (u >> 10));
|
|
21003
|
-
out[c++] = String.fromCharCode(56320 + (u & 1023));
|
|
21004
|
-
} else {
|
|
21005
|
-
const c2 = bytes[pos++];
|
|
21006
|
-
const c3 = bytes[pos++];
|
|
21007
|
-
out[c++] = String.fromCharCode((c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
|
|
21008
|
-
}
|
|
21009
|
-
}
|
|
21010
|
-
return out.join("");
|
|
21011
|
-
};
|
|
21012
|
-
var base64 = {
|
|
21013
|
-
/**
|
|
21014
|
-
* Maps bytes to characters.
|
|
21015
|
-
*/
|
|
21016
|
-
byteToCharMap_: null,
|
|
21017
|
-
/**
|
|
21018
|
-
* Maps characters to bytes.
|
|
21019
|
-
*/
|
|
21020
|
-
charToByteMap_: null,
|
|
21021
|
-
/**
|
|
21022
|
-
* Maps bytes to websafe characters.
|
|
21023
|
-
* @private
|
|
21024
|
-
*/
|
|
21025
|
-
byteToCharMapWebSafe_: null,
|
|
21026
|
-
/**
|
|
21027
|
-
* Maps websafe characters to bytes.
|
|
21028
|
-
* @private
|
|
21029
|
-
*/
|
|
21030
|
-
charToByteMapWebSafe_: null,
|
|
21031
|
-
/**
|
|
21032
|
-
* Our default alphabet, shared between
|
|
21033
|
-
* ENCODED_VALS and ENCODED_VALS_WEBSAFE
|
|
21034
|
-
*/
|
|
21035
|
-
ENCODED_VALS_BASE: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
|
21036
|
-
/**
|
|
21037
|
-
* Our default alphabet. Value 64 (=) is special; it means "nothing."
|
|
21038
|
-
*/
|
|
21039
|
-
get ENCODED_VALS() {
|
|
21040
|
-
return this.ENCODED_VALS_BASE + "+/=";
|
|
21041
|
-
},
|
|
21042
|
-
/**
|
|
21043
|
-
* Our websafe alphabet.
|
|
21044
|
-
*/
|
|
21045
|
-
get ENCODED_VALS_WEBSAFE() {
|
|
21046
|
-
return this.ENCODED_VALS_BASE + "-_.";
|
|
21047
|
-
},
|
|
21048
|
-
/**
|
|
21049
|
-
* Whether this browser supports the atob and btoa functions. This extension
|
|
21050
|
-
* started at Mozilla but is now implemented by many browsers. We use the
|
|
21051
|
-
* ASSUME_* variables to avoid pulling in the full useragent detection library
|
|
21052
|
-
* but still allowing the standard per-browser compilations.
|
|
21053
|
-
*
|
|
21054
|
-
*/
|
|
21055
|
-
HAS_NATIVE_SUPPORT: typeof atob === "function",
|
|
21056
|
-
/**
|
|
21057
|
-
* Base64-encode an array of bytes.
|
|
21058
|
-
*
|
|
21059
|
-
* @param input An array of bytes (numbers with
|
|
21060
|
-
* value in [0, 255]) to encode.
|
|
21061
|
-
* @param webSafe Boolean indicating we should use the
|
|
21062
|
-
* alternative alphabet.
|
|
21063
|
-
* @return The base64 encoded string.
|
|
21064
|
-
*/
|
|
21065
|
-
encodeByteArray(input, webSafe) {
|
|
21066
|
-
if (!Array.isArray(input)) {
|
|
21067
|
-
throw Error("encodeByteArray takes an array as a parameter");
|
|
21068
|
-
}
|
|
21069
|
-
this.init_();
|
|
21070
|
-
const byteToCharMap = webSafe ? this.byteToCharMapWebSafe_ : this.byteToCharMap_;
|
|
21071
|
-
const output = [];
|
|
21072
|
-
for (let i = 0; i < input.length; i += 3) {
|
|
21073
|
-
const byte1 = input[i];
|
|
21074
|
-
const haveByte2 = i + 1 < input.length;
|
|
21075
|
-
const byte2 = haveByte2 ? input[i + 1] : 0;
|
|
21076
|
-
const haveByte3 = i + 2 < input.length;
|
|
21077
|
-
const byte3 = haveByte3 ? input[i + 2] : 0;
|
|
21078
|
-
const outByte1 = byte1 >> 2;
|
|
21079
|
-
const outByte2 = (byte1 & 3) << 4 | byte2 >> 4;
|
|
21080
|
-
let outByte3 = (byte2 & 15) << 2 | byte3 >> 6;
|
|
21081
|
-
let outByte4 = byte3 & 63;
|
|
21082
|
-
if (!haveByte3) {
|
|
21083
|
-
outByte4 = 64;
|
|
21084
|
-
if (!haveByte2) {
|
|
21085
|
-
outByte3 = 64;
|
|
21086
|
-
}
|
|
21087
|
-
}
|
|
21088
|
-
output.push(byteToCharMap[outByte1], byteToCharMap[outByte2], byteToCharMap[outByte3], byteToCharMap[outByte4]);
|
|
21089
|
-
}
|
|
21090
|
-
return output.join("");
|
|
21091
|
-
},
|
|
21092
|
-
/**
|
|
21093
|
-
* Base64-encode a string.
|
|
21094
|
-
*
|
|
21095
|
-
* @param input A string to encode.
|
|
21096
|
-
* @param webSafe If true, we should use the
|
|
21097
|
-
* alternative alphabet.
|
|
21098
|
-
* @return The base64 encoded string.
|
|
21099
|
-
*/
|
|
21100
|
-
encodeString(input, webSafe) {
|
|
21101
|
-
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
|
|
21102
|
-
return btoa(input);
|
|
21103
|
-
}
|
|
21104
|
-
return this.encodeByteArray(stringToByteArray$1(input), webSafe);
|
|
21105
|
-
},
|
|
21106
|
-
/**
|
|
21107
|
-
* Base64-decode a string.
|
|
21108
|
-
*
|
|
21109
|
-
* @param input to decode.
|
|
21110
|
-
* @param webSafe True if we should use the
|
|
21111
|
-
* alternative alphabet.
|
|
21112
|
-
* @return string representing the decoded value.
|
|
21113
|
-
*/
|
|
21114
|
-
decodeString(input, webSafe) {
|
|
21115
|
-
if (this.HAS_NATIVE_SUPPORT && !webSafe) {
|
|
21116
|
-
return atob(input);
|
|
21117
|
-
}
|
|
21118
|
-
return byteArrayToString(this.decodeStringToByteArray(input, webSafe));
|
|
21119
|
-
},
|
|
21120
|
-
/**
|
|
21121
|
-
* Base64-decode a string.
|
|
21122
|
-
*
|
|
21123
|
-
* In base-64 decoding, groups of four characters are converted into three
|
|
21124
|
-
* bytes. If the encoder did not apply padding, the input length may not
|
|
21125
|
-
* be a multiple of 4.
|
|
21126
|
-
*
|
|
21127
|
-
* In this case, the last group will have fewer than 4 characters, and
|
|
21128
|
-
* padding will be inferred. If the group has one or two characters, it decodes
|
|
21129
|
-
* to one byte. If the group has three characters, it decodes to two bytes.
|
|
21130
|
-
*
|
|
21131
|
-
* @param input Input to decode.
|
|
21132
|
-
* @param webSafe True if we should use the web-safe alphabet.
|
|
21133
|
-
* @return bytes representing the decoded value.
|
|
21134
|
-
*/
|
|
21135
|
-
decodeStringToByteArray(input, webSafe) {
|
|
21136
|
-
this.init_();
|
|
21137
|
-
const charToByteMap = webSafe ? this.charToByteMapWebSafe_ : this.charToByteMap_;
|
|
21138
|
-
const output = [];
|
|
21139
|
-
for (let i = 0; i < input.length; ) {
|
|
21140
|
-
const byte1 = charToByteMap[input.charAt(i++)];
|
|
21141
|
-
const haveByte2 = i < input.length;
|
|
21142
|
-
const byte2 = haveByte2 ? charToByteMap[input.charAt(i)] : 0;
|
|
21143
|
-
++i;
|
|
21144
|
-
const haveByte3 = i < input.length;
|
|
21145
|
-
const byte3 = haveByte3 ? charToByteMap[input.charAt(i)] : 64;
|
|
21146
|
-
++i;
|
|
21147
|
-
const haveByte4 = i < input.length;
|
|
21148
|
-
const byte4 = haveByte4 ? charToByteMap[input.charAt(i)] : 64;
|
|
21149
|
-
++i;
|
|
21150
|
-
if (byte1 == null || byte2 == null || byte3 == null || byte4 == null) {
|
|
21151
|
-
throw new DecodeBase64StringError();
|
|
21152
|
-
}
|
|
21153
|
-
const outByte1 = byte1 << 2 | byte2 >> 4;
|
|
21154
|
-
output.push(outByte1);
|
|
21155
|
-
if (byte3 !== 64) {
|
|
21156
|
-
const outByte2 = byte2 << 4 & 240 | byte3 >> 2;
|
|
21157
|
-
output.push(outByte2);
|
|
21158
|
-
if (byte4 !== 64) {
|
|
21159
|
-
const outByte3 = byte3 << 6 & 192 | byte4;
|
|
21160
|
-
output.push(outByte3);
|
|
21161
|
-
}
|
|
21162
|
-
}
|
|
21163
|
-
}
|
|
21164
|
-
return output;
|
|
21165
|
-
},
|
|
21166
|
-
/**
|
|
21167
|
-
* Lazy static initialization function. Called before
|
|
21168
|
-
* accessing any of the static map variables.
|
|
21169
|
-
* @private
|
|
21170
|
-
*/
|
|
21171
|
-
init_() {
|
|
21172
|
-
if (!this.byteToCharMap_) {
|
|
21173
|
-
this.byteToCharMap_ = {};
|
|
21174
|
-
this.charToByteMap_ = {};
|
|
21175
|
-
this.byteToCharMapWebSafe_ = {};
|
|
21176
|
-
this.charToByteMapWebSafe_ = {};
|
|
21177
|
-
for (let i = 0; i < this.ENCODED_VALS.length; i++) {
|
|
21178
|
-
this.byteToCharMap_[i] = this.ENCODED_VALS.charAt(i);
|
|
21179
|
-
this.charToByteMap_[this.byteToCharMap_[i]] = i;
|
|
21180
|
-
this.byteToCharMapWebSafe_[i] = this.ENCODED_VALS_WEBSAFE.charAt(i);
|
|
21181
|
-
this.charToByteMapWebSafe_[this.byteToCharMapWebSafe_[i]] = i;
|
|
21182
|
-
if (i >= this.ENCODED_VALS_BASE.length) {
|
|
21183
|
-
this.charToByteMap_[this.ENCODED_VALS_WEBSAFE.charAt(i)] = i;
|
|
21184
|
-
this.charToByteMapWebSafe_[this.ENCODED_VALS.charAt(i)] = i;
|
|
21185
|
-
}
|
|
21186
|
-
}
|
|
21187
|
-
}
|
|
21188
|
-
}
|
|
21189
|
-
};
|
|
21190
|
-
var DecodeBase64StringError = class extends Error {
|
|
21191
|
-
constructor() {
|
|
21192
|
-
super(...arguments);
|
|
21193
|
-
this.name = "DecodeBase64StringError";
|
|
21194
|
-
}
|
|
21195
|
-
};
|
|
21196
|
-
var base64Encode = function(str) {
|
|
21197
|
-
const utf8Bytes = stringToByteArray$1(str);
|
|
21198
|
-
return base64.encodeByteArray(utf8Bytes, true);
|
|
20616
|
+
var base64Encode = function(str) {
|
|
20617
|
+
const utf8Bytes = stringToByteArray$1(str);
|
|
20618
|
+
return base64.encodeByteArray(utf8Bytes, true);
|
|
21199
20619
|
};
|
|
21200
20620
|
var base64urlEncodeWithoutPadding = function(str) {
|
|
21201
20621
|
return base64Encode(str).replace(/\./g, "");
|
|
@@ -23052,7 +22472,7 @@ function arrayEquals(left, right, comparator) {
|
|
|
23052
22472
|
}
|
|
23053
22473
|
var MIN_SECONDS = -62135596800;
|
|
23054
22474
|
var MS_TO_NANOS = 1e6;
|
|
23055
|
-
var
|
|
22475
|
+
var Timestamp = class _Timestamp {
|
|
23056
22476
|
/**
|
|
23057
22477
|
* Creates a new timestamp with the current date, with millisecond precision.
|
|
23058
22478
|
*
|
|
@@ -23171,10 +22591,10 @@ var SnapshotVersion = class _SnapshotVersion {
|
|
|
23171
22591
|
return new _SnapshotVersion(value);
|
|
23172
22592
|
}
|
|
23173
22593
|
static min() {
|
|
23174
|
-
return new _SnapshotVersion(new
|
|
22594
|
+
return new _SnapshotVersion(new Timestamp(0, 0));
|
|
23175
22595
|
}
|
|
23176
22596
|
static max() {
|
|
23177
|
-
return new _SnapshotVersion(new
|
|
22597
|
+
return new _SnapshotVersion(new Timestamp(253402300799, 1e9 - 1));
|
|
23178
22598
|
}
|
|
23179
22599
|
constructor(timestamp) {
|
|
23180
22600
|
this.timestamp = timestamp;
|
|
@@ -23516,7 +22936,7 @@ FieldIndex.UNKNOWN_ID = -1;
|
|
|
23516
22936
|
function newIndexOffsetSuccessorFromReadTime(readTime, largestBatchId) {
|
|
23517
22937
|
const successorSeconds = readTime.toTimestamp().seconds;
|
|
23518
22938
|
const successorNanos = readTime.toTimestamp().nanoseconds + 1;
|
|
23519
|
-
const successor = SnapshotVersion.fromTimestamp(successorNanos === 1e9 ? new
|
|
22939
|
+
const successor = SnapshotVersion.fromTimestamp(successorNanos === 1e9 ? new Timestamp(successorSeconds + 1, 0) : new Timestamp(successorSeconds, successorNanos));
|
|
23520
22940
|
return new IndexOffset(successor, DocumentKey.empty(), largestBatchId);
|
|
23521
22941
|
}
|
|
23522
22942
|
function newIndexOffsetFromDocument(document2) {
|
|
@@ -24563,7 +23983,7 @@ function getPreviousValue(value) {
|
|
|
24563
23983
|
}
|
|
24564
23984
|
function getLocalWriteTime(value) {
|
|
24565
23985
|
const localWriteTime = normalizeTimestamp(value.mapValue.fields[LOCAL_WRITE_TIME_KEY].timestampValue);
|
|
24566
|
-
return new
|
|
23986
|
+
return new Timestamp(localWriteTime.seconds, localWriteTime.nanos);
|
|
24567
23987
|
}
|
|
24568
23988
|
var DEFAULT_DATABASE_NAME = "(default)";
|
|
24569
23989
|
var DatabaseId = class _DatabaseId {
|
|
@@ -26577,7 +25997,7 @@ function fromInt32Proto(val) {
|
|
|
26577
25997
|
}
|
|
26578
25998
|
function fromTimestamp(date) {
|
|
26579
25999
|
const timestamp = normalizeTimestamp(date);
|
|
26580
|
-
return new
|
|
26000
|
+
return new Timestamp(timestamp.seconds, timestamp.nanos);
|
|
26581
26001
|
}
|
|
26582
26002
|
function fromVersion(version3) {
|
|
26583
26003
|
hardAssert(!!version3);
|
|
@@ -27149,7 +26569,7 @@ var LocalDocumentsView = class {
|
|
|
27149
26569
|
return this.remoteDocumentCache.getEntry(transaction, key);
|
|
27150
26570
|
}).next((document2) => {
|
|
27151
26571
|
if (overlay !== null) {
|
|
27152
|
-
mutationApplyToLocalView(overlay.mutation, document2, FieldMask.empty(),
|
|
26572
|
+
mutationApplyToLocalView(overlay.mutation, document2, FieldMask.empty(), Timestamp.now());
|
|
27153
26573
|
}
|
|
27154
26574
|
return document2;
|
|
27155
26575
|
});
|
|
@@ -27233,7 +26653,7 @@ var LocalDocumentsView = class {
|
|
|
27233
26653
|
recalculateDocuments = recalculateDocuments.insert(doc.key, doc);
|
|
27234
26654
|
} else if (overlay !== void 0) {
|
|
27235
26655
|
mutatedFields.set(doc.key, overlay.mutation.getFieldMask());
|
|
27236
|
-
mutationApplyToLocalView(overlay.mutation, doc, overlay.mutation.getFieldMask(),
|
|
26656
|
+
mutationApplyToLocalView(overlay.mutation, doc, overlay.mutation.getFieldMask(), Timestamp.now());
|
|
27237
26657
|
} else {
|
|
27238
26658
|
mutatedFields.set(doc.key, FieldMask.empty());
|
|
27239
26659
|
}
|
|
@@ -27389,7 +26809,7 @@ var LocalDocumentsView = class {
|
|
|
27389
26809
|
remoteDocuments.forEach((key, document2) => {
|
|
27390
26810
|
const overlay = overlays.get(key);
|
|
27391
26811
|
if (overlay !== void 0) {
|
|
27392
|
-
mutationApplyToLocalView(overlay.mutation, document2, FieldMask.empty(),
|
|
26812
|
+
mutationApplyToLocalView(overlay.mutation, document2, FieldMask.empty(), Timestamp.now());
|
|
27393
26813
|
}
|
|
27394
26814
|
if (queryMatches(query, document2)) {
|
|
27395
26815
|
results = results.insert(key, document2);
|
|
@@ -32497,240 +31917,1060 @@ var Firestore$1 = class {
|
|
|
32497
31917
|
} else {
|
|
32498
31918
|
this._terminateTask = "notTerminated";
|
|
32499
31919
|
}
|
|
32500
|
-
}
|
|
32501
|
-
/** Returns a JSON-serializable representation of this `Firestore` instance. */
|
|
32502
|
-
toJSON() {
|
|
32503
|
-
return {
|
|
32504
|
-
app: this._app,
|
|
32505
|
-
databaseId: this._databaseId,
|
|
32506
|
-
settings: this._settings
|
|
32507
|
-
};
|
|
31920
|
+
}
|
|
31921
|
+
/** Returns a JSON-serializable representation of this `Firestore` instance. */
|
|
31922
|
+
toJSON() {
|
|
31923
|
+
return {
|
|
31924
|
+
app: this._app,
|
|
31925
|
+
databaseId: this._databaseId,
|
|
31926
|
+
settings: this._settings
|
|
31927
|
+
};
|
|
31928
|
+
}
|
|
31929
|
+
/**
|
|
31930
|
+
* Terminates all components used by this client. Subclasses can override
|
|
31931
|
+
* this method to clean up their own dependencies, but must also call this
|
|
31932
|
+
* method.
|
|
31933
|
+
*
|
|
31934
|
+
* Only ever called once.
|
|
31935
|
+
*/
|
|
31936
|
+
_terminate() {
|
|
31937
|
+
removeComponents(this);
|
|
31938
|
+
return Promise.resolve();
|
|
31939
|
+
}
|
|
31940
|
+
};
|
|
31941
|
+
var LOG_TAG = "AsyncQueue";
|
|
31942
|
+
var AsyncQueueImpl = class {
|
|
31943
|
+
constructor(tail = Promise.resolve()) {
|
|
31944
|
+
this.retryableOps = [];
|
|
31945
|
+
this._isShuttingDown = false;
|
|
31946
|
+
this.delayedOperations = [];
|
|
31947
|
+
this.failure = null;
|
|
31948
|
+
this.operationInProgress = false;
|
|
31949
|
+
this.skipNonRestrictedTasks = false;
|
|
31950
|
+
this.timerIdsToSkip = [];
|
|
31951
|
+
this.backoff = new ExponentialBackoff(
|
|
31952
|
+
this,
|
|
31953
|
+
"async_queue_retry"
|
|
31954
|
+
/* TimerId.AsyncQueueRetry */
|
|
31955
|
+
);
|
|
31956
|
+
this.visibilityHandler = () => {
|
|
31957
|
+
this.backoff.skipBackoff();
|
|
31958
|
+
};
|
|
31959
|
+
this.tail = tail;
|
|
31960
|
+
}
|
|
31961
|
+
get isShuttingDown() {
|
|
31962
|
+
return this._isShuttingDown;
|
|
31963
|
+
}
|
|
31964
|
+
/**
|
|
31965
|
+
* Adds a new operation to the queue without waiting for it to complete (i.e.
|
|
31966
|
+
* we ignore the Promise result).
|
|
31967
|
+
*/
|
|
31968
|
+
enqueueAndForget(op) {
|
|
31969
|
+
this.enqueue(op);
|
|
31970
|
+
}
|
|
31971
|
+
enqueueAndForgetEvenWhileRestricted(op) {
|
|
31972
|
+
this.verifyNotFailed();
|
|
31973
|
+
this.enqueueInternal(op);
|
|
31974
|
+
}
|
|
31975
|
+
enterRestrictedMode(purgeExistingTasks) {
|
|
31976
|
+
if (!this._isShuttingDown) {
|
|
31977
|
+
this._isShuttingDown = true;
|
|
31978
|
+
this.skipNonRestrictedTasks = purgeExistingTasks || false;
|
|
31979
|
+
}
|
|
31980
|
+
}
|
|
31981
|
+
enqueue(op) {
|
|
31982
|
+
this.verifyNotFailed();
|
|
31983
|
+
if (this._isShuttingDown) {
|
|
31984
|
+
return new Promise(() => {
|
|
31985
|
+
});
|
|
31986
|
+
}
|
|
31987
|
+
const task = new Deferred2();
|
|
31988
|
+
return this.enqueueInternal(() => {
|
|
31989
|
+
if (this._isShuttingDown && this.skipNonRestrictedTasks) {
|
|
31990
|
+
return Promise.resolve();
|
|
31991
|
+
}
|
|
31992
|
+
op().then(task.resolve, task.reject);
|
|
31993
|
+
return task.promise;
|
|
31994
|
+
}).then(() => task.promise);
|
|
31995
|
+
}
|
|
31996
|
+
enqueueRetryable(op) {
|
|
31997
|
+
this.enqueueAndForget(() => {
|
|
31998
|
+
this.retryableOps.push(op);
|
|
31999
|
+
return this.retryNextOp();
|
|
32000
|
+
});
|
|
32001
|
+
}
|
|
32002
|
+
/**
|
|
32003
|
+
* Runs the next operation from the retryable queue. If the operation fails,
|
|
32004
|
+
* reschedules with backoff.
|
|
32005
|
+
*/
|
|
32006
|
+
async retryNextOp() {
|
|
32007
|
+
if (this.retryableOps.length === 0) {
|
|
32008
|
+
return;
|
|
32009
|
+
}
|
|
32010
|
+
try {
|
|
32011
|
+
await this.retryableOps[0]();
|
|
32012
|
+
this.retryableOps.shift();
|
|
32013
|
+
this.backoff.reset();
|
|
32014
|
+
} catch (e) {
|
|
32015
|
+
if (isIndexedDbTransactionError(e)) {
|
|
32016
|
+
logDebug(LOG_TAG, "Operation failed with retryable error: " + e);
|
|
32017
|
+
} else {
|
|
32018
|
+
throw e;
|
|
32019
|
+
}
|
|
32020
|
+
}
|
|
32021
|
+
if (this.retryableOps.length > 0) {
|
|
32022
|
+
this.backoff.backoffAndRun(() => this.retryNextOp());
|
|
32023
|
+
}
|
|
32024
|
+
}
|
|
32025
|
+
enqueueInternal(op) {
|
|
32026
|
+
const newTail = this.tail.then(() => {
|
|
32027
|
+
this.operationInProgress = true;
|
|
32028
|
+
return op().catch((error) => {
|
|
32029
|
+
this.failure = error;
|
|
32030
|
+
this.operationInProgress = false;
|
|
32031
|
+
const message = getMessageOrStack(error);
|
|
32032
|
+
logError("INTERNAL UNHANDLED ERROR: ", message);
|
|
32033
|
+
throw error;
|
|
32034
|
+
}).then((result) => {
|
|
32035
|
+
this.operationInProgress = false;
|
|
32036
|
+
return result;
|
|
32037
|
+
});
|
|
32038
|
+
});
|
|
32039
|
+
this.tail = newTail;
|
|
32040
|
+
return newTail;
|
|
32041
|
+
}
|
|
32042
|
+
enqueueAfterDelay(timerId, delayMs, op) {
|
|
32043
|
+
this.verifyNotFailed();
|
|
32044
|
+
if (this.timerIdsToSkip.indexOf(timerId) > -1) {
|
|
32045
|
+
delayMs = 0;
|
|
32046
|
+
}
|
|
32047
|
+
const delayedOp = DelayedOperation.createAndSchedule(this, timerId, delayMs, op, (removedOp) => this.removeDelayedOperation(removedOp));
|
|
32048
|
+
this.delayedOperations.push(delayedOp);
|
|
32049
|
+
return delayedOp;
|
|
32050
|
+
}
|
|
32051
|
+
verifyNotFailed() {
|
|
32052
|
+
if (this.failure) {
|
|
32053
|
+
fail();
|
|
32054
|
+
}
|
|
32055
|
+
}
|
|
32056
|
+
verifyOperationInProgress() {
|
|
32057
|
+
}
|
|
32058
|
+
/**
|
|
32059
|
+
* Waits until all currently queued tasks are finished executing. Delayed
|
|
32060
|
+
* operations are not run.
|
|
32061
|
+
*/
|
|
32062
|
+
async drain() {
|
|
32063
|
+
let currentTail;
|
|
32064
|
+
do {
|
|
32065
|
+
currentTail = this.tail;
|
|
32066
|
+
await currentTail;
|
|
32067
|
+
} while (currentTail !== this.tail);
|
|
32068
|
+
}
|
|
32069
|
+
/**
|
|
32070
|
+
* For Tests: Determine if a delayed operation with a particular TimerId
|
|
32071
|
+
* exists.
|
|
32072
|
+
*/
|
|
32073
|
+
containsDelayedOperation(timerId) {
|
|
32074
|
+
for (const op of this.delayedOperations) {
|
|
32075
|
+
if (op.timerId === timerId) {
|
|
32076
|
+
return true;
|
|
32077
|
+
}
|
|
32078
|
+
}
|
|
32079
|
+
return false;
|
|
32080
|
+
}
|
|
32081
|
+
/**
|
|
32082
|
+
* For Tests: Runs some or all delayed operations early.
|
|
32083
|
+
*
|
|
32084
|
+
* @param lastTimerId - Delayed operations up to and including this TimerId
|
|
32085
|
+
* will be drained. Pass TimerId.All to run all delayed operations.
|
|
32086
|
+
* @returns a Promise that resolves once all operations have been run.
|
|
32087
|
+
*/
|
|
32088
|
+
runAllDelayedOperationsUntil(lastTimerId) {
|
|
32089
|
+
return this.drain().then(() => {
|
|
32090
|
+
this.delayedOperations.sort((a, b) => a.targetTimeMs - b.targetTimeMs);
|
|
32091
|
+
for (const op of this.delayedOperations) {
|
|
32092
|
+
op.skipDelay();
|
|
32093
|
+
if (lastTimerId !== "all" && op.timerId === lastTimerId) {
|
|
32094
|
+
break;
|
|
32095
|
+
}
|
|
32096
|
+
}
|
|
32097
|
+
return this.drain();
|
|
32098
|
+
});
|
|
32099
|
+
}
|
|
32100
|
+
/**
|
|
32101
|
+
* For Tests: Skip all subsequent delays for a timer id.
|
|
32102
|
+
*/
|
|
32103
|
+
skipDelaysForTimerId(timerId) {
|
|
32104
|
+
this.timerIdsToSkip.push(timerId);
|
|
32105
|
+
}
|
|
32106
|
+
/** Called once a DelayedOperation is run or canceled. */
|
|
32107
|
+
removeDelayedOperation(op) {
|
|
32108
|
+
const index = this.delayedOperations.indexOf(op);
|
|
32109
|
+
this.delayedOperations.splice(index, 1);
|
|
32110
|
+
}
|
|
32111
|
+
};
|
|
32112
|
+
function getMessageOrStack(error) {
|
|
32113
|
+
let message = error.message || "";
|
|
32114
|
+
if (error.stack) {
|
|
32115
|
+
if (error.stack.includes(error.message)) {
|
|
32116
|
+
message = error.stack;
|
|
32117
|
+
} else {
|
|
32118
|
+
message = error.message + "\n" + error.stack;
|
|
32119
|
+
}
|
|
32120
|
+
}
|
|
32121
|
+
return message;
|
|
32122
|
+
}
|
|
32123
|
+
var Firestore = class extends Firestore$1 {
|
|
32124
|
+
/** @hideconstructor */
|
|
32125
|
+
constructor(authCredentialsProvider, appCheckCredentialsProvider, databaseId, app) {
|
|
32126
|
+
super(authCredentialsProvider, appCheckCredentialsProvider, databaseId, app);
|
|
32127
|
+
this.type = "firestore";
|
|
32128
|
+
this._queue = new AsyncQueueImpl();
|
|
32129
|
+
this._persistenceKey = (app === null || app === void 0 ? void 0 : app.name) || "[DEFAULT]";
|
|
32130
|
+
}
|
|
32131
|
+
async _terminate() {
|
|
32132
|
+
if (this._firestoreClient) {
|
|
32133
|
+
const terminate = this._firestoreClient.terminate();
|
|
32134
|
+
this._queue = new AsyncQueueImpl(terminate);
|
|
32135
|
+
this._firestoreClient = void 0;
|
|
32136
|
+
await terminate;
|
|
32137
|
+
}
|
|
32138
|
+
}
|
|
32139
|
+
};
|
|
32140
|
+
function registerFirestore(variant, useFetchStreams = true) {
|
|
32141
|
+
setSDKVersion(SDK_VERSION);
|
|
32142
|
+
_registerComponent(new Component("firestore", (container, { instanceIdentifier: databaseId, options: settings }) => {
|
|
32143
|
+
const app = container.getProvider("app").getImmediate();
|
|
32144
|
+
const firestoreInstance = new Firestore(new FirebaseAuthCredentialsProvider(container.getProvider("auth-internal")), new FirebaseAppCheckTokenProvider(container.getProvider("app-check-internal")), databaseIdFromApp(app, databaseId), app);
|
|
32145
|
+
settings = Object.assign({ useFetchStreams }, settings);
|
|
32146
|
+
firestoreInstance._setSettings(settings);
|
|
32147
|
+
return firestoreInstance;
|
|
32148
|
+
}, "PUBLIC").setMultipleInstances(true));
|
|
32149
|
+
registerVersion(name2, version$12, variant);
|
|
32150
|
+
registerVersion(name2, version$12, "esm2017");
|
|
32151
|
+
}
|
|
32152
|
+
var FIELD_PATH_RESERVED = new RegExp("[~\\*/\\[\\]]");
|
|
32153
|
+
registerFirestore("node");
|
|
32154
|
+
|
|
32155
|
+
// src/admin/booking/booking.calculator.ts
|
|
32156
|
+
var import_firestore = require("firebase/firestore");
|
|
32157
|
+
var BookingAvailabilityCalculator = class {
|
|
32158
|
+
/**
|
|
32159
|
+
* Calculate available booking slots based on the provided data
|
|
32160
|
+
*
|
|
32161
|
+
* @param request - The request containing all necessary data for calculation
|
|
32162
|
+
* @returns Response with available booking slots
|
|
32163
|
+
*/
|
|
32164
|
+
static calculateSlots(request) {
|
|
32165
|
+
const {
|
|
32166
|
+
clinic,
|
|
32167
|
+
practitioner,
|
|
32168
|
+
procedure,
|
|
32169
|
+
timeframe,
|
|
32170
|
+
clinicCalendarEvents,
|
|
32171
|
+
practitionerCalendarEvents
|
|
32172
|
+
} = request;
|
|
32173
|
+
const schedulingIntervalMinutes = clinic.schedulingInterval || this.DEFAULT_INTERVAL_MINUTES;
|
|
32174
|
+
const procedureDurationMinutes = procedure.duration;
|
|
32175
|
+
console.log(
|
|
32176
|
+
`Calculating slots with interval: ${schedulingIntervalMinutes}min and procedure duration: ${procedureDurationMinutes}min`
|
|
32177
|
+
);
|
|
32178
|
+
let availableIntervals = [
|
|
32179
|
+
{ start: timeframe.start, end: timeframe.end }
|
|
32180
|
+
];
|
|
32181
|
+
availableIntervals = this.applyClinicWorkingHours(
|
|
32182
|
+
availableIntervals,
|
|
32183
|
+
clinic.workingHours,
|
|
32184
|
+
timeframe
|
|
32185
|
+
);
|
|
32186
|
+
availableIntervals = this.subtractBlockingEvents(
|
|
32187
|
+
availableIntervals,
|
|
32188
|
+
clinicCalendarEvents
|
|
32189
|
+
);
|
|
32190
|
+
availableIntervals = this.applyPractitionerWorkingHours(
|
|
32191
|
+
availableIntervals,
|
|
32192
|
+
practitioner,
|
|
32193
|
+
clinic.id,
|
|
32194
|
+
timeframe
|
|
32195
|
+
);
|
|
32196
|
+
availableIntervals = this.subtractPractitionerBusyTimes(
|
|
32197
|
+
availableIntervals,
|
|
32198
|
+
practitionerCalendarEvents
|
|
32199
|
+
);
|
|
32200
|
+
console.log(
|
|
32201
|
+
`After all filters, have ${availableIntervals.length} available intervals`
|
|
32202
|
+
);
|
|
32203
|
+
const availableSlots = this.generateAvailableSlots(
|
|
32204
|
+
availableIntervals,
|
|
32205
|
+
schedulingIntervalMinutes,
|
|
32206
|
+
procedureDurationMinutes
|
|
32207
|
+
);
|
|
32208
|
+
return { availableSlots };
|
|
32209
|
+
}
|
|
32210
|
+
/**
|
|
32211
|
+
* Apply clinic working hours to available intervals
|
|
32212
|
+
*
|
|
32213
|
+
* @param intervals - Current available intervals
|
|
32214
|
+
* @param workingHours - Clinic working hours
|
|
32215
|
+
* @param timeframe - Overall timeframe being considered
|
|
32216
|
+
* @returns Intervals filtered by clinic working hours
|
|
32217
|
+
*/
|
|
32218
|
+
static applyClinicWorkingHours(intervals, workingHours, timeframe) {
|
|
32219
|
+
if (!intervals.length) return [];
|
|
32220
|
+
console.log(
|
|
32221
|
+
`Applying clinic working hours to ${intervals.length} intervals`
|
|
32222
|
+
);
|
|
32223
|
+
const workingIntervals = this.createWorkingHoursIntervals(
|
|
32224
|
+
workingHours,
|
|
32225
|
+
timeframe.start.toDate(),
|
|
32226
|
+
timeframe.end.toDate()
|
|
32227
|
+
);
|
|
32228
|
+
return this.intersectIntervals(intervals, workingIntervals);
|
|
32229
|
+
}
|
|
32230
|
+
/**
|
|
32231
|
+
* Create time intervals for working hours across multiple days
|
|
32232
|
+
*
|
|
32233
|
+
* @param workingHours - Working hours definition
|
|
32234
|
+
* @param startDate - Start date of the overall timeframe
|
|
32235
|
+
* @param endDate - End date of the overall timeframe
|
|
32236
|
+
* @returns Array of time intervals representing working hours
|
|
32237
|
+
*/
|
|
32238
|
+
static createWorkingHoursIntervals(workingHours, startDate, endDate) {
|
|
32239
|
+
const workingIntervals = [];
|
|
32240
|
+
const currentDate = new Date(startDate);
|
|
32241
|
+
currentDate.setHours(0, 0, 0, 0);
|
|
32242
|
+
const dayNameToNumber = {
|
|
32243
|
+
sunday: 0,
|
|
32244
|
+
monday: 1,
|
|
32245
|
+
tuesday: 2,
|
|
32246
|
+
wednesday: 3,
|
|
32247
|
+
thursday: 4,
|
|
32248
|
+
friday: 5,
|
|
32249
|
+
saturday: 6
|
|
32250
|
+
};
|
|
32251
|
+
while (currentDate <= endDate) {
|
|
32252
|
+
const dayOfWeek = currentDate.getDay();
|
|
32253
|
+
const dayName = Object.keys(dayNameToNumber).find(
|
|
32254
|
+
(key) => dayNameToNumber[key] === dayOfWeek
|
|
32255
|
+
);
|
|
32256
|
+
if (dayName && workingHours[dayName]) {
|
|
32257
|
+
const daySchedule = workingHours[dayName];
|
|
32258
|
+
if (daySchedule) {
|
|
32259
|
+
const [openHours, openMinutes] = daySchedule.open.split(":").map(Number);
|
|
32260
|
+
const [closeHours, closeMinutes] = daySchedule.close.split(":").map(Number);
|
|
32261
|
+
const workStart = new Date(currentDate);
|
|
32262
|
+
workStart.setHours(openHours, openMinutes, 0, 0);
|
|
32263
|
+
const workEnd = new Date(currentDate);
|
|
32264
|
+
workEnd.setHours(closeHours, closeMinutes, 0, 0);
|
|
32265
|
+
if (workEnd > startDate && workStart < endDate) {
|
|
32266
|
+
const intervalStart = workStart < startDate ? startDate : workStart;
|
|
32267
|
+
const intervalEnd = workEnd > endDate ? endDate : workEnd;
|
|
32268
|
+
workingIntervals.push({
|
|
32269
|
+
start: import_firestore.Timestamp.fromDate(intervalStart),
|
|
32270
|
+
end: import_firestore.Timestamp.fromDate(intervalEnd)
|
|
32271
|
+
});
|
|
32272
|
+
if (daySchedule.breaks && daySchedule.breaks.length > 0) {
|
|
32273
|
+
for (const breakTime of daySchedule.breaks) {
|
|
32274
|
+
const [breakStartHours, breakStartMinutes] = breakTime.start.split(":").map(Number);
|
|
32275
|
+
const [breakEndHours, breakEndMinutes] = breakTime.end.split(":").map(Number);
|
|
32276
|
+
const breakStart = new Date(currentDate);
|
|
32277
|
+
breakStart.setHours(breakStartHours, breakStartMinutes, 0, 0);
|
|
32278
|
+
const breakEnd = new Date(currentDate);
|
|
32279
|
+
breakEnd.setHours(breakEndHours, breakEndMinutes, 0, 0);
|
|
32280
|
+
workingIntervals.splice(
|
|
32281
|
+
-1,
|
|
32282
|
+
1,
|
|
32283
|
+
...this.subtractInterval(
|
|
32284
|
+
workingIntervals[workingIntervals.length - 1],
|
|
32285
|
+
{
|
|
32286
|
+
start: import_firestore.Timestamp.fromDate(breakStart),
|
|
32287
|
+
end: import_firestore.Timestamp.fromDate(breakEnd)
|
|
32288
|
+
}
|
|
32289
|
+
)
|
|
32290
|
+
);
|
|
32291
|
+
}
|
|
32292
|
+
}
|
|
32293
|
+
}
|
|
32294
|
+
}
|
|
32295
|
+
}
|
|
32296
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
32297
|
+
}
|
|
32298
|
+
return workingIntervals;
|
|
32299
|
+
}
|
|
32300
|
+
/**
|
|
32301
|
+
* Subtract blocking events from available intervals
|
|
32302
|
+
*
|
|
32303
|
+
* @param intervals - Current available intervals
|
|
32304
|
+
* @param events - Calendar events to subtract
|
|
32305
|
+
* @returns Available intervals after removing blocking events
|
|
32306
|
+
*/
|
|
32307
|
+
static subtractBlockingEvents(intervals, events) {
|
|
32308
|
+
if (!intervals.length) return [];
|
|
32309
|
+
console.log(`Subtracting ${events.length} blocking events`);
|
|
32310
|
+
const blockingEvents = events.filter(
|
|
32311
|
+
(event) => event.eventType === "blocking" /* BLOCKING */ || event.eventType === "break" /* BREAK */ || event.eventType === "free_day" /* FREE_DAY */
|
|
32312
|
+
);
|
|
32313
|
+
let result = [...intervals];
|
|
32314
|
+
for (const event of blockingEvents) {
|
|
32315
|
+
const { start, end } = event.eventTime;
|
|
32316
|
+
const blockingInterval = { start, end };
|
|
32317
|
+
const newResult = [];
|
|
32318
|
+
for (const interval of result) {
|
|
32319
|
+
const remainingIntervals = this.subtractInterval(
|
|
32320
|
+
interval,
|
|
32321
|
+
blockingInterval
|
|
32322
|
+
);
|
|
32323
|
+
newResult.push(...remainingIntervals);
|
|
32324
|
+
}
|
|
32325
|
+
result = newResult;
|
|
32326
|
+
}
|
|
32327
|
+
return result;
|
|
32328
|
+
}
|
|
32329
|
+
/**
|
|
32330
|
+
* Apply practitioner's specific working hours for the given clinic
|
|
32331
|
+
*
|
|
32332
|
+
* @param intervals - Current available intervals
|
|
32333
|
+
* @param practitioner - Practitioner object
|
|
32334
|
+
* @param clinicId - ID of the clinic
|
|
32335
|
+
* @param timeframe - Overall timeframe being considered
|
|
32336
|
+
* @returns Intervals filtered by practitioner's working hours
|
|
32337
|
+
*/
|
|
32338
|
+
static applyPractitionerWorkingHours(intervals, practitioner, clinicId, timeframe) {
|
|
32339
|
+
if (!intervals.length) return [];
|
|
32340
|
+
console.log(`Applying practitioner working hours for clinic ${clinicId}`);
|
|
32341
|
+
const clinicWorkingHours = practitioner.clinicWorkingHours.find(
|
|
32342
|
+
(hours) => hours.clinicId === clinicId && hours.isActive
|
|
32343
|
+
);
|
|
32344
|
+
if (!clinicWorkingHours) {
|
|
32345
|
+
console.log(
|
|
32346
|
+
`No working hours found for practitioner at clinic ${clinicId}`
|
|
32347
|
+
);
|
|
32348
|
+
return [];
|
|
32349
|
+
}
|
|
32350
|
+
const workingIntervals = this.createPractitionerWorkingHoursIntervals(
|
|
32351
|
+
clinicWorkingHours.workingHours,
|
|
32352
|
+
timeframe.start.toDate(),
|
|
32353
|
+
timeframe.end.toDate()
|
|
32354
|
+
);
|
|
32355
|
+
return this.intersectIntervals(intervals, workingIntervals);
|
|
32356
|
+
}
|
|
32357
|
+
/**
|
|
32358
|
+
* Create time intervals for practitioner's working hours across multiple days
|
|
32359
|
+
*
|
|
32360
|
+
* @param workingHours - Practitioner's working hours definition
|
|
32361
|
+
* @param startDate - Start date of the overall timeframe
|
|
32362
|
+
* @param endDate - End date of the overall timeframe
|
|
32363
|
+
* @returns Array of time intervals representing practitioner's working hours
|
|
32364
|
+
*/
|
|
32365
|
+
static createPractitionerWorkingHoursIntervals(workingHours, startDate, endDate) {
|
|
32366
|
+
const workingIntervals = [];
|
|
32367
|
+
const currentDate = new Date(startDate);
|
|
32368
|
+
currentDate.setHours(0, 0, 0, 0);
|
|
32369
|
+
const dayNameToNumber = {
|
|
32370
|
+
sunday: 0,
|
|
32371
|
+
monday: 1,
|
|
32372
|
+
tuesday: 2,
|
|
32373
|
+
wednesday: 3,
|
|
32374
|
+
thursday: 4,
|
|
32375
|
+
friday: 5,
|
|
32376
|
+
saturday: 6
|
|
32377
|
+
};
|
|
32378
|
+
while (currentDate <= endDate) {
|
|
32379
|
+
const dayOfWeek = currentDate.getDay();
|
|
32380
|
+
const dayName = Object.keys(dayNameToNumber).find(
|
|
32381
|
+
(key) => dayNameToNumber[key] === dayOfWeek
|
|
32382
|
+
);
|
|
32383
|
+
if (dayName && workingHours[dayName]) {
|
|
32384
|
+
const daySchedule = workingHours[dayName];
|
|
32385
|
+
if (daySchedule) {
|
|
32386
|
+
const [startHours, startMinutes] = daySchedule.start.split(":").map(Number);
|
|
32387
|
+
const [endHours, endMinutes] = daySchedule.end.split(":").map(Number);
|
|
32388
|
+
const workStart = new Date(currentDate);
|
|
32389
|
+
workStart.setHours(startHours, startMinutes, 0, 0);
|
|
32390
|
+
const workEnd = new Date(currentDate);
|
|
32391
|
+
workEnd.setHours(endHours, endMinutes, 0, 0);
|
|
32392
|
+
if (workEnd > startDate && workStart < endDate) {
|
|
32393
|
+
const intervalStart = workStart < startDate ? startDate : workStart;
|
|
32394
|
+
const intervalEnd = workEnd > endDate ? endDate : workEnd;
|
|
32395
|
+
workingIntervals.push({
|
|
32396
|
+
start: import_firestore.Timestamp.fromDate(intervalStart),
|
|
32397
|
+
end: import_firestore.Timestamp.fromDate(intervalEnd)
|
|
32398
|
+
});
|
|
32399
|
+
}
|
|
32400
|
+
}
|
|
32401
|
+
}
|
|
32402
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
32403
|
+
}
|
|
32404
|
+
return workingIntervals;
|
|
32405
|
+
}
|
|
32406
|
+
/**
|
|
32407
|
+
* Subtract practitioner's busy times from available intervals
|
|
32408
|
+
*
|
|
32409
|
+
* @param intervals - Current available intervals
|
|
32410
|
+
* @param events - Practitioner's calendar events
|
|
32411
|
+
* @returns Available intervals after removing busy times
|
|
32412
|
+
*/
|
|
32413
|
+
static subtractPractitionerBusyTimes(intervals, events) {
|
|
32414
|
+
if (!intervals.length) return [];
|
|
32415
|
+
console.log(`Subtracting ${events.length} practitioner events`);
|
|
32416
|
+
const busyEvents = events.filter(
|
|
32417
|
+
(event) => (
|
|
32418
|
+
// Include all blocking events
|
|
32419
|
+
event.eventType === "blocking" /* BLOCKING */ || event.eventType === "break" /* BREAK */ || event.eventType === "free_day" /* FREE_DAY */ || // Include appointments that are pending, confirmed, or rescheduled
|
|
32420
|
+
event.eventType === "appointment" /* APPOINTMENT */ && (event.status === "pending" /* PENDING */ || event.status === "confirmed" /* CONFIRMED */ || event.status === "rescheduled" /* RESCHEDULED */)
|
|
32421
|
+
)
|
|
32422
|
+
);
|
|
32423
|
+
let result = [...intervals];
|
|
32424
|
+
for (const event of busyEvents) {
|
|
32425
|
+
const { start, end } = event.eventTime;
|
|
32426
|
+
const busyInterval = { start, end };
|
|
32427
|
+
const newResult = [];
|
|
32428
|
+
for (const interval of result) {
|
|
32429
|
+
const remainingIntervals = this.subtractInterval(
|
|
32430
|
+
interval,
|
|
32431
|
+
busyInterval
|
|
32432
|
+
);
|
|
32433
|
+
newResult.push(...remainingIntervals);
|
|
32434
|
+
}
|
|
32435
|
+
result = newResult;
|
|
32436
|
+
}
|
|
32437
|
+
return result;
|
|
32508
32438
|
}
|
|
32509
32439
|
/**
|
|
32510
|
-
*
|
|
32511
|
-
* this method to clean up their own dependencies, but must also call this
|
|
32512
|
-
* method.
|
|
32440
|
+
* Generate available booking slots based on the final available intervals
|
|
32513
32441
|
*
|
|
32514
|
-
*
|
|
32442
|
+
* @param intervals - Final available intervals
|
|
32443
|
+
* @param intervalMinutes - Scheduling interval in minutes
|
|
32444
|
+
* @param durationMinutes - Procedure duration in minutes
|
|
32445
|
+
* @returns Array of available booking slots
|
|
32515
32446
|
*/
|
|
32516
|
-
|
|
32517
|
-
|
|
32518
|
-
|
|
32519
|
-
|
|
32520
|
-
};
|
|
32521
|
-
var LOG_TAG = "AsyncQueue";
|
|
32522
|
-
var AsyncQueueImpl = class {
|
|
32523
|
-
constructor(tail = Promise.resolve()) {
|
|
32524
|
-
this.retryableOps = [];
|
|
32525
|
-
this._isShuttingDown = false;
|
|
32526
|
-
this.delayedOperations = [];
|
|
32527
|
-
this.failure = null;
|
|
32528
|
-
this.operationInProgress = false;
|
|
32529
|
-
this.skipNonRestrictedTasks = false;
|
|
32530
|
-
this.timerIdsToSkip = [];
|
|
32531
|
-
this.backoff = new ExponentialBackoff(
|
|
32532
|
-
this,
|
|
32533
|
-
"async_queue_retry"
|
|
32534
|
-
/* TimerId.AsyncQueueRetry */
|
|
32447
|
+
static generateAvailableSlots(intervals, intervalMinutes, durationMinutes) {
|
|
32448
|
+
const slots = [];
|
|
32449
|
+
console.log(
|
|
32450
|
+
`Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure`
|
|
32535
32451
|
);
|
|
32536
|
-
|
|
32537
|
-
|
|
32538
|
-
|
|
32539
|
-
|
|
32540
|
-
|
|
32541
|
-
|
|
32542
|
-
|
|
32452
|
+
const durationMs = durationMinutes * 60 * 1e3;
|
|
32453
|
+
const intervalMs = intervalMinutes * 60 * 1e3;
|
|
32454
|
+
for (const interval of intervals) {
|
|
32455
|
+
const intervalStart = interval.start.toDate();
|
|
32456
|
+
const intervalEnd = interval.end.toDate();
|
|
32457
|
+
let slotStart = new Date(intervalStart);
|
|
32458
|
+
const minutesIntoDay = slotStart.getHours() * 60 + slotStart.getMinutes();
|
|
32459
|
+
const minutesRemainder = minutesIntoDay % intervalMinutes;
|
|
32460
|
+
if (minutesRemainder > 0) {
|
|
32461
|
+
slotStart.setMinutes(
|
|
32462
|
+
slotStart.getMinutes() + (intervalMinutes - minutesRemainder)
|
|
32463
|
+
);
|
|
32464
|
+
}
|
|
32465
|
+
while (slotStart.getTime() + durationMs <= intervalEnd.getTime()) {
|
|
32466
|
+
const slotEnd = new Date(slotStart.getTime() + durationMs);
|
|
32467
|
+
if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
|
|
32468
|
+
slots.push({
|
|
32469
|
+
start: import_firestore.Timestamp.fromDate(slotStart)
|
|
32470
|
+
});
|
|
32471
|
+
}
|
|
32472
|
+
slotStart = new Date(slotStart.getTime() + intervalMs);
|
|
32473
|
+
}
|
|
32474
|
+
}
|
|
32475
|
+
console.log(`Generated ${slots.length} available slots`);
|
|
32476
|
+
return slots;
|
|
32543
32477
|
}
|
|
32544
32478
|
/**
|
|
32545
|
-
*
|
|
32546
|
-
*
|
|
32479
|
+
* Check if a time slot is fully available within the given intervals
|
|
32480
|
+
*
|
|
32481
|
+
* @param slotStart - Start time of the slot
|
|
32482
|
+
* @param slotEnd - End time of the slot
|
|
32483
|
+
* @param intervals - Available intervals
|
|
32484
|
+
* @returns True if the slot is fully contained within an available interval
|
|
32547
32485
|
*/
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
32551
|
-
|
|
32552
|
-
|
|
32553
|
-
|
|
32486
|
+
static isSlotFullyAvailable(slotStart, slotEnd, intervals) {
|
|
32487
|
+
return intervals.some((interval) => {
|
|
32488
|
+
const intervalStart = interval.start.toDate();
|
|
32489
|
+
const intervalEnd = interval.end.toDate();
|
|
32490
|
+
return slotStart >= intervalStart && slotEnd <= intervalEnd;
|
|
32491
|
+
});
|
|
32554
32492
|
}
|
|
32555
|
-
|
|
32556
|
-
|
|
32557
|
-
|
|
32558
|
-
|
|
32493
|
+
/**
|
|
32494
|
+
* Intersect two sets of time intervals
|
|
32495
|
+
*
|
|
32496
|
+
* @param intervalsA - First set of intervals
|
|
32497
|
+
* @param intervalsB - Second set of intervals
|
|
32498
|
+
* @returns Intersection of the two sets of intervals
|
|
32499
|
+
*/
|
|
32500
|
+
static intersectIntervals(intervalsA, intervalsB) {
|
|
32501
|
+
const result = [];
|
|
32502
|
+
for (const intervalA of intervalsA) {
|
|
32503
|
+
for (const intervalB of intervalsB) {
|
|
32504
|
+
const intersectionStart = intervalA.start.toMillis() > intervalB.start.toMillis() ? intervalA.start : intervalB.start;
|
|
32505
|
+
const intersectionEnd = intervalA.end.toMillis() < intervalB.end.toMillis() ? intervalA.end : intervalB.end;
|
|
32506
|
+
if (intersectionStart.toMillis() < intersectionEnd.toMillis()) {
|
|
32507
|
+
result.push({
|
|
32508
|
+
start: intersectionStart,
|
|
32509
|
+
end: intersectionEnd
|
|
32510
|
+
});
|
|
32511
|
+
}
|
|
32512
|
+
}
|
|
32559
32513
|
}
|
|
32514
|
+
return this.mergeOverlappingIntervals(result);
|
|
32560
32515
|
}
|
|
32561
|
-
|
|
32562
|
-
|
|
32563
|
-
|
|
32564
|
-
|
|
32565
|
-
|
|
32516
|
+
/**
|
|
32517
|
+
* Subtract one interval from another, potentially resulting in 0, 1, or 2 intervals
|
|
32518
|
+
*
|
|
32519
|
+
* @param interval - Interval to subtract from
|
|
32520
|
+
* @param subtrahend - Interval to subtract
|
|
32521
|
+
* @returns Array of remaining intervals after subtraction
|
|
32522
|
+
*/
|
|
32523
|
+
static subtractInterval(interval, subtrahend) {
|
|
32524
|
+
if (interval.end.toMillis() <= subtrahend.start.toMillis() || interval.start.toMillis() >= subtrahend.end.toMillis()) {
|
|
32525
|
+
return [interval];
|
|
32566
32526
|
}
|
|
32567
|
-
|
|
32568
|
-
|
|
32569
|
-
|
|
32570
|
-
|
|
32527
|
+
if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() >= interval.end.toMillis()) {
|
|
32528
|
+
return [];
|
|
32529
|
+
}
|
|
32530
|
+
if (subtrahend.start.toMillis() > interval.start.toMillis() && subtrahend.end.toMillis() < interval.end.toMillis()) {
|
|
32531
|
+
return [
|
|
32532
|
+
{
|
|
32533
|
+
start: interval.start,
|
|
32534
|
+
end: subtrahend.start
|
|
32535
|
+
},
|
|
32536
|
+
{
|
|
32537
|
+
start: subtrahend.end,
|
|
32538
|
+
end: interval.end
|
|
32539
|
+
}
|
|
32540
|
+
];
|
|
32541
|
+
}
|
|
32542
|
+
if (subtrahend.start.toMillis() <= interval.start.toMillis() && subtrahend.end.toMillis() > interval.start.toMillis()) {
|
|
32543
|
+
return [
|
|
32544
|
+
{
|
|
32545
|
+
start: subtrahend.end,
|
|
32546
|
+
end: interval.end
|
|
32547
|
+
}
|
|
32548
|
+
];
|
|
32549
|
+
}
|
|
32550
|
+
return [
|
|
32551
|
+
{
|
|
32552
|
+
start: interval.start,
|
|
32553
|
+
end: subtrahend.start
|
|
32571
32554
|
}
|
|
32572
|
-
|
|
32573
|
-
return task.promise;
|
|
32574
|
-
}).then(() => task.promise);
|
|
32575
|
-
}
|
|
32576
|
-
enqueueRetryable(op) {
|
|
32577
|
-
this.enqueueAndForget(() => {
|
|
32578
|
-
this.retryableOps.push(op);
|
|
32579
|
-
return this.retryNextOp();
|
|
32580
|
-
});
|
|
32555
|
+
];
|
|
32581
32556
|
}
|
|
32582
32557
|
/**
|
|
32583
|
-
*
|
|
32584
|
-
*
|
|
32558
|
+
* Merge overlapping intervals to simplify the result
|
|
32559
|
+
*
|
|
32560
|
+
* @param intervals - Intervals to merge
|
|
32561
|
+
* @returns Merged intervals
|
|
32585
32562
|
*/
|
|
32586
|
-
|
|
32587
|
-
if (
|
|
32588
|
-
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
32592
|
-
|
|
32593
|
-
|
|
32594
|
-
|
|
32595
|
-
if (
|
|
32596
|
-
|
|
32563
|
+
static mergeOverlappingIntervals(intervals) {
|
|
32564
|
+
if (intervals.length <= 1) return intervals;
|
|
32565
|
+
const sorted = [...intervals].sort(
|
|
32566
|
+
(a, b) => a.start.toMillis() - b.start.toMillis()
|
|
32567
|
+
);
|
|
32568
|
+
const result = [sorted[0]];
|
|
32569
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
32570
|
+
const current = sorted[i];
|
|
32571
|
+
const lastResult = result[result.length - 1];
|
|
32572
|
+
if (current.start.toMillis() <= lastResult.end.toMillis()) {
|
|
32573
|
+
if (current.end.toMillis() > lastResult.end.toMillis()) {
|
|
32574
|
+
lastResult.end = current.end;
|
|
32575
|
+
}
|
|
32597
32576
|
} else {
|
|
32598
|
-
|
|
32577
|
+
result.push(current);
|
|
32599
32578
|
}
|
|
32600
32579
|
}
|
|
32601
|
-
|
|
32602
|
-
this.backoff.backoffAndRun(() => this.retryNextOp());
|
|
32603
|
-
}
|
|
32604
|
-
}
|
|
32605
|
-
enqueueInternal(op) {
|
|
32606
|
-
const newTail = this.tail.then(() => {
|
|
32607
|
-
this.operationInProgress = true;
|
|
32608
|
-
return op().catch((error) => {
|
|
32609
|
-
this.failure = error;
|
|
32610
|
-
this.operationInProgress = false;
|
|
32611
|
-
const message = getMessageOrStack(error);
|
|
32612
|
-
logError("INTERNAL UNHANDLED ERROR: ", message);
|
|
32613
|
-
throw error;
|
|
32614
|
-
}).then((result) => {
|
|
32615
|
-
this.operationInProgress = false;
|
|
32616
|
-
return result;
|
|
32617
|
-
});
|
|
32618
|
-
});
|
|
32619
|
-
this.tail = newTail;
|
|
32620
|
-
return newTail;
|
|
32580
|
+
return result;
|
|
32621
32581
|
}
|
|
32622
|
-
|
|
32623
|
-
|
|
32624
|
-
|
|
32625
|
-
|
|
32626
|
-
|
|
32627
|
-
|
|
32628
|
-
|
|
32629
|
-
|
|
32582
|
+
};
|
|
32583
|
+
/** Default scheduling interval in minutes if not specified by the clinic */
|
|
32584
|
+
BookingAvailabilityCalculator.DEFAULT_INTERVAL_MINUTES = 15;
|
|
32585
|
+
|
|
32586
|
+
// src/types/appointment/index.ts
|
|
32587
|
+
var APPOINTMENTS_COLLECTION = "appointments";
|
|
32588
|
+
|
|
32589
|
+
// src/admin/booking/booking.admin.ts
|
|
32590
|
+
var BookingAdmin = class {
|
|
32591
|
+
/**
|
|
32592
|
+
* Creates a new BookingAdmin instance
|
|
32593
|
+
* @param firestore - Firestore instance provided by the caller
|
|
32594
|
+
*/
|
|
32595
|
+
constructor(firestore9) {
|
|
32596
|
+
this.db = firestore9 || admin7.firestore();
|
|
32630
32597
|
}
|
|
32631
|
-
|
|
32632
|
-
|
|
32633
|
-
|
|
32598
|
+
/**
|
|
32599
|
+
* Gets available booking time slots for a specific clinic, practitioner, and procedure
|
|
32600
|
+
*
|
|
32601
|
+
* @param clinicId - ID of the clinic
|
|
32602
|
+
* @param practitionerId - ID of the practitioner
|
|
32603
|
+
* @param procedureId - ID of the procedure
|
|
32604
|
+
* @param timeframe - Time range to check for availability
|
|
32605
|
+
* @returns Promise resolving to an array of available booking slots
|
|
32606
|
+
*/
|
|
32607
|
+
async getAvailableBookingSlots(clinicId, practitionerId, procedureId, timeframe) {
|
|
32608
|
+
try {
|
|
32609
|
+
console.log(
|
|
32610
|
+
`[BookingAdmin] Getting available slots for clinic ${clinicId}, practitioner ${practitionerId}, procedure ${procedureId}`
|
|
32611
|
+
);
|
|
32612
|
+
const start = timeframe.start instanceof Date ? admin7.firestore.Timestamp.fromDate(timeframe.start) : timeframe.start;
|
|
32613
|
+
const end = timeframe.end instanceof Date ? admin7.firestore.Timestamp.fromDate(timeframe.end) : timeframe.end;
|
|
32614
|
+
const clinicDoc = await this.db.collection("clinics").doc(clinicId).get();
|
|
32615
|
+
if (!clinicDoc.exists) {
|
|
32616
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
32617
|
+
}
|
|
32618
|
+
const clinic = clinicDoc.data();
|
|
32619
|
+
const practitionerDoc = await this.db.collection("practitioners").doc(practitionerId).get();
|
|
32620
|
+
if (!practitionerDoc.exists) {
|
|
32621
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
32622
|
+
}
|
|
32623
|
+
const practitioner = practitionerDoc.data();
|
|
32624
|
+
const procedureDoc = await this.db.collection("procedures").doc(procedureId).get();
|
|
32625
|
+
if (!procedureDoc.exists) {
|
|
32626
|
+
throw new Error(`Procedure ${procedureId} not found`);
|
|
32627
|
+
}
|
|
32628
|
+
const procedure = procedureDoc.data();
|
|
32629
|
+
const clinicCalendarEvents = await this.getClinicCalendarEvents(
|
|
32630
|
+
clinicId,
|
|
32631
|
+
start,
|
|
32632
|
+
end
|
|
32633
|
+
);
|
|
32634
|
+
const practitionerCalendarEvents = await this.getPractitionerCalendarEvents(practitionerId, start, end);
|
|
32635
|
+
const convertedTimeframe = {
|
|
32636
|
+
start: this.adminTimestampToClientTimestamp(start),
|
|
32637
|
+
end: this.adminTimestampToClientTimestamp(end)
|
|
32638
|
+
};
|
|
32639
|
+
const request = {
|
|
32640
|
+
clinic,
|
|
32641
|
+
practitioner,
|
|
32642
|
+
procedure,
|
|
32643
|
+
timeframe: convertedTimeframe,
|
|
32644
|
+
clinicCalendarEvents: this.convertEventsTimestamps(clinicCalendarEvents),
|
|
32645
|
+
practitionerCalendarEvents: this.convertEventsTimestamps(
|
|
32646
|
+
practitionerCalendarEvents
|
|
32647
|
+
)
|
|
32648
|
+
};
|
|
32649
|
+
const result = BookingAvailabilityCalculator.calculateSlots(request);
|
|
32650
|
+
return {
|
|
32651
|
+
availableSlots: result.availableSlots.map((slot) => ({
|
|
32652
|
+
start: admin7.firestore.Timestamp.fromMillis(slot.start.toMillis())
|
|
32653
|
+
}))
|
|
32654
|
+
};
|
|
32655
|
+
} catch (error) {
|
|
32656
|
+
console.error("[BookingAdmin] Error getting available slots:", error);
|
|
32657
|
+
throw error;
|
|
32634
32658
|
}
|
|
32635
32659
|
}
|
|
32636
|
-
verifyOperationInProgress() {
|
|
32637
|
-
}
|
|
32638
32660
|
/**
|
|
32639
|
-
*
|
|
32640
|
-
* operations are not run.
|
|
32661
|
+
* Converts an admin Firestore Timestamp to a client Firestore Timestamp
|
|
32641
32662
|
*/
|
|
32642
|
-
|
|
32643
|
-
|
|
32644
|
-
|
|
32645
|
-
|
|
32646
|
-
|
|
32647
|
-
|
|
32663
|
+
adminTimestampToClientTimestamp(timestamp) {
|
|
32664
|
+
return {
|
|
32665
|
+
seconds: timestamp.seconds,
|
|
32666
|
+
nanoseconds: timestamp.nanoseconds,
|
|
32667
|
+
toDate: () => timestamp.toDate(),
|
|
32668
|
+
toMillis: () => timestamp.toMillis(),
|
|
32669
|
+
valueOf: () => timestamp.valueOf()
|
|
32670
|
+
// Add any other required methods/properties
|
|
32671
|
+
};
|
|
32648
32672
|
}
|
|
32649
32673
|
/**
|
|
32650
|
-
*
|
|
32651
|
-
* exists.
|
|
32674
|
+
* Converts timestamps in calendar events from admin Firestore Timestamps to client Firestore Timestamps
|
|
32652
32675
|
*/
|
|
32653
|
-
|
|
32654
|
-
|
|
32655
|
-
|
|
32656
|
-
|
|
32676
|
+
convertEventsTimestamps(events) {
|
|
32677
|
+
return events.map((event) => ({
|
|
32678
|
+
...event,
|
|
32679
|
+
eventTime: {
|
|
32680
|
+
start: this.adminTimestampToClientTimestamp(event.eventTime.start),
|
|
32681
|
+
end: this.adminTimestampToClientTimestamp(event.eventTime.end)
|
|
32657
32682
|
}
|
|
32658
|
-
|
|
32659
|
-
|
|
32683
|
+
// Convert any other timestamps in the event if needed
|
|
32684
|
+
}));
|
|
32660
32685
|
}
|
|
32661
32686
|
/**
|
|
32662
|
-
*
|
|
32687
|
+
* Fetches clinic calendar events for a specific time range
|
|
32663
32688
|
*
|
|
32664
|
-
* @param
|
|
32665
|
-
*
|
|
32666
|
-
* @
|
|
32689
|
+
* @param clinicId - ID of the clinic
|
|
32690
|
+
* @param start - Start time of the range
|
|
32691
|
+
* @param end - End time of the range
|
|
32692
|
+
* @returns Promise resolving to an array of calendar events
|
|
32667
32693
|
*/
|
|
32668
|
-
|
|
32669
|
-
|
|
32670
|
-
this.
|
|
32671
|
-
|
|
32672
|
-
|
|
32673
|
-
|
|
32674
|
-
|
|
32675
|
-
|
|
32676
|
-
|
|
32677
|
-
|
|
32678
|
-
|
|
32694
|
+
async getClinicCalendarEvents(clinicId, start, end) {
|
|
32695
|
+
try {
|
|
32696
|
+
const eventsRef = this.db.collection(`clinics/${clinicId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
|
|
32697
|
+
const snapshot = await eventsRef.get();
|
|
32698
|
+
return snapshot.docs.map((doc) => ({
|
|
32699
|
+
...doc.data(),
|
|
32700
|
+
id: doc.id
|
|
32701
|
+
}));
|
|
32702
|
+
} catch (error) {
|
|
32703
|
+
console.error(
|
|
32704
|
+
`[BookingAdmin] Error fetching clinic calendar events:`,
|
|
32705
|
+
error
|
|
32706
|
+
);
|
|
32707
|
+
return [];
|
|
32708
|
+
}
|
|
32679
32709
|
}
|
|
32680
32710
|
/**
|
|
32681
|
-
*
|
|
32711
|
+
* Fetches practitioner calendar events for a specific time range
|
|
32712
|
+
*
|
|
32713
|
+
* @param practitionerId - ID of the practitioner
|
|
32714
|
+
* @param start - Start time of the range
|
|
32715
|
+
* @param end - End time of the range
|
|
32716
|
+
* @returns Promise resolving to an array of calendar events
|
|
32682
32717
|
*/
|
|
32683
|
-
|
|
32684
|
-
|
|
32685
|
-
|
|
32686
|
-
|
|
32687
|
-
|
|
32688
|
-
|
|
32689
|
-
|
|
32690
|
-
|
|
32691
|
-
}
|
|
32692
|
-
|
|
32693
|
-
|
|
32694
|
-
|
|
32695
|
-
|
|
32696
|
-
|
|
32697
|
-
} else {
|
|
32698
|
-
message = error.message + "\n" + error.stack;
|
|
32718
|
+
async getPractitionerCalendarEvents(practitionerId, start, end) {
|
|
32719
|
+
try {
|
|
32720
|
+
const eventsRef = this.db.collection(`practitioners/${practitionerId}/calendar`).where("eventTime.start", ">=", start).where("eventTime.start", "<=", end);
|
|
32721
|
+
const snapshot = await eventsRef.get();
|
|
32722
|
+
return snapshot.docs.map((doc) => ({
|
|
32723
|
+
...doc.data(),
|
|
32724
|
+
id: doc.id
|
|
32725
|
+
}));
|
|
32726
|
+
} catch (error) {
|
|
32727
|
+
console.error(
|
|
32728
|
+
`[BookingAdmin] Error fetching practitioner calendar events:`,
|
|
32729
|
+
error
|
|
32730
|
+
);
|
|
32731
|
+
return [];
|
|
32699
32732
|
}
|
|
32700
32733
|
}
|
|
32701
|
-
|
|
32702
|
-
|
|
32703
|
-
|
|
32704
|
-
|
|
32705
|
-
|
|
32706
|
-
|
|
32707
|
-
|
|
32708
|
-
|
|
32709
|
-
|
|
32710
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
32714
|
-
|
|
32715
|
-
|
|
32716
|
-
|
|
32734
|
+
/**
|
|
32735
|
+
* Orchestrates the creation of a new appointment, including data aggregation.
|
|
32736
|
+
* This method is intended to be called from a trusted backend environment (e.g., an Express route handler in a Cloud Function).
|
|
32737
|
+
*
|
|
32738
|
+
* @param data - Data required to create the appointment.
|
|
32739
|
+
* @param authenticatedUserId - The ID of the user making the request (for auditing, and usually is the patientId).
|
|
32740
|
+
* @returns Promise resolving to an object indicating success, and appointmentId or an error message.
|
|
32741
|
+
*/
|
|
32742
|
+
async orchestrateAppointmentCreation(data, authenticatedUserId) {
|
|
32743
|
+
var _a;
|
|
32744
|
+
console.log(
|
|
32745
|
+
`[BookingAdmin] Orchestrating appointment creation for patient ${data.patientId} by user ${authenticatedUserId}`
|
|
32746
|
+
);
|
|
32747
|
+
try {
|
|
32748
|
+
if (!data.patientId || !data.procedureId || !data.appointmentStartTime || !data.appointmentEndTime) {
|
|
32749
|
+
return {
|
|
32750
|
+
success: false,
|
|
32751
|
+
error: "Missing required fields: patientId, procedureId, appointmentStartTime, or appointmentEndTime."
|
|
32752
|
+
};
|
|
32753
|
+
}
|
|
32754
|
+
if (data.appointmentEndTime.toMillis() <= data.appointmentStartTime.toMillis()) {
|
|
32755
|
+
return {
|
|
32756
|
+
success: false,
|
|
32757
|
+
error: "Appointment end time must be after start time."
|
|
32758
|
+
};
|
|
32759
|
+
}
|
|
32760
|
+
if (authenticatedUserId !== data.patientId) {
|
|
32761
|
+
console.warn(
|
|
32762
|
+
`[BookingAdmin] Authenticated user ${authenticatedUserId} is booking for a different patient ${data.patientId}. Review authorization if this is not intended.`
|
|
32763
|
+
);
|
|
32764
|
+
}
|
|
32765
|
+
const procedureRef = this.db.collection(PROCEDURES_COLLECTION).doc(data.procedureId);
|
|
32766
|
+
const procedureDoc = await procedureRef.get();
|
|
32767
|
+
if (!procedureDoc.exists) {
|
|
32768
|
+
return {
|
|
32769
|
+
success: false,
|
|
32770
|
+
error: `Procedure ${data.procedureId} not found.`
|
|
32771
|
+
};
|
|
32772
|
+
}
|
|
32773
|
+
const procedure = procedureDoc.data();
|
|
32774
|
+
const clinicRef = this.db.collection(CLINICS_COLLECTION).doc(procedure.clinicBranchId);
|
|
32775
|
+
const clinicSnap = await clinicRef.get();
|
|
32776
|
+
if (!clinicSnap.exists) {
|
|
32777
|
+
return {
|
|
32778
|
+
success: false,
|
|
32779
|
+
error: `Clinic ${procedure.clinicBranchId} not found.`
|
|
32780
|
+
};
|
|
32781
|
+
}
|
|
32782
|
+
const clinicData = clinicSnap.data();
|
|
32783
|
+
const practitionerRef = this.db.collection(PRACTITIONERS_COLLECTION).doc(procedure.practitionerId);
|
|
32784
|
+
const patientProfileRef = this.db.collection(PATIENTS_COLLECTION).doc(data.patientId);
|
|
32785
|
+
const patientSensitiveRef = this.db.collection(PATIENTS_COLLECTION).doc(data.patientId).collection(PATIENT_SENSITIVE_INFO_COLLECTION).doc(data.patientId);
|
|
32786
|
+
const clinicGroupRef = this.db.collection(CLINIC_GROUPS_COLLECTION).doc(clinicData.clinicGroupId);
|
|
32787
|
+
const [
|
|
32788
|
+
practitionerSnap,
|
|
32789
|
+
patientProfileSnap,
|
|
32790
|
+
patientSensitiveSnap,
|
|
32791
|
+
clinicGroupSnap
|
|
32792
|
+
] = await Promise.all([
|
|
32793
|
+
// clinicRef.get() is already done via clinicSnap
|
|
32794
|
+
practitionerRef.get(),
|
|
32795
|
+
patientProfileRef.get(),
|
|
32796
|
+
patientSensitiveRef.get(),
|
|
32797
|
+
clinicGroupRef.get()
|
|
32798
|
+
// Fetch the defined clinicGroupRef
|
|
32799
|
+
]);
|
|
32800
|
+
if (!practitionerSnap.exists)
|
|
32801
|
+
return {
|
|
32802
|
+
success: false,
|
|
32803
|
+
error: `Practitioner ${procedure.practitionerId} not found.`
|
|
32804
|
+
};
|
|
32805
|
+
if (!patientProfileSnap.exists)
|
|
32806
|
+
return {
|
|
32807
|
+
success: false,
|
|
32808
|
+
error: `PatientProfile ${data.patientId} not found.`
|
|
32809
|
+
};
|
|
32810
|
+
if (!clinicGroupSnap.exists)
|
|
32811
|
+
return {
|
|
32812
|
+
success: false,
|
|
32813
|
+
error: `ClinicGroup for clinic ${procedure.clinicBranchId} not found.`
|
|
32814
|
+
};
|
|
32815
|
+
const practitionerData = practitionerSnap.data();
|
|
32816
|
+
const patientProfileData = patientProfileSnap.data();
|
|
32817
|
+
const patientSensitiveData = patientSensitiveSnap.exists ? patientSensitiveSnap.data() : void 0;
|
|
32818
|
+
const clinicGroupData = clinicGroupSnap.data();
|
|
32819
|
+
const autoConfirm = clinicGroupData.autoConfirmAppointments || false;
|
|
32820
|
+
const initialStatus = autoConfirm ? "confirmed" /* CONFIRMED */ : "pending" /* PENDING */;
|
|
32821
|
+
const clinicInfo = {
|
|
32822
|
+
id: clinicSnap.id,
|
|
32823
|
+
name: clinicData.name,
|
|
32824
|
+
featuredPhoto: clinicData.coverPhoto || clinicData.logo || "",
|
|
32825
|
+
description: clinicData.description,
|
|
32826
|
+
location: clinicData.location,
|
|
32827
|
+
contactInfo: clinicData.contactInfo
|
|
32828
|
+
};
|
|
32829
|
+
const practitionerInfo = {
|
|
32830
|
+
id: practitionerSnap.id,
|
|
32831
|
+
practitionerPhoto: practitionerData.basicInfo.profileImageUrl || null,
|
|
32832
|
+
name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
32833
|
+
email: practitionerData.basicInfo.email,
|
|
32834
|
+
phone: practitionerData.basicInfo.phoneNumber || null,
|
|
32835
|
+
certification: practitionerData.certification
|
|
32836
|
+
};
|
|
32837
|
+
const patientInfo = {
|
|
32838
|
+
id: patientProfileSnap.id,
|
|
32839
|
+
fullName: `${(patientSensitiveData == null ? void 0 : patientSensitiveData.firstName) || ""} ${(patientSensitiveData == null ? void 0 : patientSensitiveData.lastName) || ""}`.trim() || patientProfileData.displayName,
|
|
32840
|
+
email: (patientSensitiveData == null ? void 0 : patientSensitiveData.email) || "",
|
|
32841
|
+
phone: (patientSensitiveData == null ? void 0 : patientSensitiveData.phoneNumber) || patientProfileData.phoneNumber || null,
|
|
32842
|
+
dateOfBirth: (patientSensitiveData == null ? void 0 : patientSensitiveData.dateOfBirth) || patientProfileData.dateOfBirth || admin7.firestore.Timestamp.now(),
|
|
32843
|
+
gender: (patientSensitiveData == null ? void 0 : patientSensitiveData.gender) || "other" /* OTHER */
|
|
32844
|
+
};
|
|
32845
|
+
const procedureCategory = procedure.category;
|
|
32846
|
+
const procedureSubCategory = procedure.subcategory;
|
|
32847
|
+
const procedureTechnology = procedure.technology;
|
|
32848
|
+
const procedureProduct = procedure.product;
|
|
32849
|
+
const procedureInfo = {
|
|
32850
|
+
id: procedure.id,
|
|
32851
|
+
name: procedure.name,
|
|
32852
|
+
description: procedure.description,
|
|
32853
|
+
family: procedure.family,
|
|
32854
|
+
categoryName: (procedureCategory == null ? void 0 : procedureCategory.name) || "",
|
|
32855
|
+
subcategoryName: (procedureSubCategory == null ? void 0 : procedureSubCategory.name) || "",
|
|
32856
|
+
technologyName: (procedureTechnology == null ? void 0 : procedureTechnology.name) || "",
|
|
32857
|
+
price: procedure.price,
|
|
32858
|
+
pricingMeasure: procedure.pricingMeasure,
|
|
32859
|
+
currency: procedure.currency,
|
|
32860
|
+
duration: procedure.duration,
|
|
32861
|
+
clinicId: procedure.clinicBranchId,
|
|
32862
|
+
clinicName: clinicData.name,
|
|
32863
|
+
practitionerId: procedure.practitionerId,
|
|
32864
|
+
practitionerName: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
|
|
32865
|
+
photo: ((_a = procedure.photos) == null ? void 0 : _a[0]) || "",
|
|
32866
|
+
brandName: (procedureProduct == null ? void 0 : procedureProduct.brandName) || "",
|
|
32867
|
+
productName: (procedureProduct == null ? void 0 : procedureProduct.name) || ""
|
|
32868
|
+
};
|
|
32869
|
+
const procedureExtendedInfo = {
|
|
32870
|
+
id: procedure.id,
|
|
32871
|
+
name: procedure.name,
|
|
32872
|
+
description: procedure.description,
|
|
32873
|
+
cost: procedure.price,
|
|
32874
|
+
duration: procedure.duration,
|
|
32875
|
+
procedureFamily: procedure.family,
|
|
32876
|
+
procedureCategoryId: (procedureCategory == null ? void 0 : procedureCategory.id) || "",
|
|
32877
|
+
procedureCategoryName: (procedureCategory == null ? void 0 : procedureCategory.name) || "",
|
|
32878
|
+
procedureSubCategoryId: (procedureSubCategory == null ? void 0 : procedureSubCategory.id) || "",
|
|
32879
|
+
procedureSubCategoryName: (procedureSubCategory == null ? void 0 : procedureSubCategory.name) || "",
|
|
32880
|
+
procedureTechnologyId: (procedureTechnology == null ? void 0 : procedureTechnology.id) || "",
|
|
32881
|
+
procedureTechnologyName: (procedureTechnology == null ? void 0 : procedureTechnology.name) || "",
|
|
32882
|
+
procedureProductBrandId: procedureProduct.brandId || "",
|
|
32883
|
+
procedureProductBrandName: procedureProduct.brandName || "",
|
|
32884
|
+
procedureProductId: procedureProduct.id || "",
|
|
32885
|
+
procedureProductName: procedureProduct.name || ""
|
|
32886
|
+
};
|
|
32887
|
+
let pendingUserFormsIds = [];
|
|
32888
|
+
let linkedFormIds = [];
|
|
32889
|
+
if (procedure.documentationTemplates && Array.isArray(procedure.documentationTemplates)) {
|
|
32890
|
+
pendingUserFormsIds = procedure.documentationTemplates.filter(
|
|
32891
|
+
(template) => template.isUserForm && template.isRequired
|
|
32892
|
+
).map((template) => template.id);
|
|
32893
|
+
linkedFormIds = procedure.documentationTemplates.map(
|
|
32894
|
+
(template) => template.id
|
|
32895
|
+
);
|
|
32896
|
+
}
|
|
32897
|
+
const newAppointmentId = this.db.collection(APPOINTMENTS_COLLECTION).doc().id;
|
|
32898
|
+
const serverTimestampValue = admin7.firestore.FieldValue.serverTimestamp();
|
|
32899
|
+
const adminTsNow = admin7.firestore.Timestamp.now();
|
|
32900
|
+
const newAppointmentData = {
|
|
32901
|
+
id: newAppointmentId,
|
|
32902
|
+
calendarEventId: "",
|
|
32903
|
+
clinicBranchId: procedure.clinicBranchId,
|
|
32904
|
+
clinicInfo,
|
|
32905
|
+
practitionerId: procedure.practitionerId,
|
|
32906
|
+
practitionerInfo,
|
|
32907
|
+
patientId: data.patientId,
|
|
32908
|
+
patientInfo,
|
|
32909
|
+
procedureId: data.procedureId,
|
|
32910
|
+
procedureInfo,
|
|
32911
|
+
procedureExtendedInfo,
|
|
32912
|
+
status: initialStatus,
|
|
32913
|
+
bookingTime: serverTimestampValue,
|
|
32914
|
+
confirmationTime: initialStatus === "confirmed" /* CONFIRMED */ ? serverTimestampValue : null,
|
|
32915
|
+
appointmentStartTime: new Timestamp(
|
|
32916
|
+
data.appointmentStartTime.seconds,
|
|
32917
|
+
data.appointmentStartTime.nanoseconds
|
|
32918
|
+
),
|
|
32919
|
+
appointmentEndTime: new Timestamp(
|
|
32920
|
+
data.appointmentEndTime.seconds,
|
|
32921
|
+
data.appointmentEndTime.nanoseconds
|
|
32922
|
+
),
|
|
32923
|
+
cost: procedure.price,
|
|
32924
|
+
currency: procedure.currency,
|
|
32925
|
+
paymentStatus: procedure.price > 0 ? "unpaid" /* UNPAID */ : "not_applicable" /* NOT_APPLICABLE */,
|
|
32926
|
+
patientNotes: data.patientNotes || null,
|
|
32927
|
+
blockingConditions: procedure.blockingConditions || [],
|
|
32928
|
+
contraindications: procedure.contraindications || [],
|
|
32929
|
+
preProcedureRequirements: procedure.preRequirements || [],
|
|
32930
|
+
postProcedureRequirements: procedure.postRequirements || [],
|
|
32931
|
+
pendingUserFormsIds,
|
|
32932
|
+
completedPreRequirements: [],
|
|
32933
|
+
completedPostRequirements: [],
|
|
32934
|
+
linkedFormIds,
|
|
32935
|
+
linkedForms: [],
|
|
32936
|
+
media: [],
|
|
32937
|
+
reviewInfo: null,
|
|
32938
|
+
finalizedDetails: void 0,
|
|
32939
|
+
internalNotes: null,
|
|
32940
|
+
cancellationReason: null,
|
|
32941
|
+
cancellationTime: null,
|
|
32942
|
+
canceledBy: void 0,
|
|
32943
|
+
rescheduleTime: null,
|
|
32944
|
+
procedureActualStartTime: null,
|
|
32945
|
+
actualDurationMinutes: void 0,
|
|
32946
|
+
isRecurring: false,
|
|
32947
|
+
recurringAppointmentId: null,
|
|
32948
|
+
isArchived: false,
|
|
32949
|
+
createdAt: serverTimestampValue,
|
|
32950
|
+
updatedAt: serverTimestampValue
|
|
32951
|
+
};
|
|
32952
|
+
await this.db.collection(APPOINTMENTS_COLLECTION).doc(newAppointmentId).set(newAppointmentData);
|
|
32953
|
+
console.log(
|
|
32954
|
+
`[BookingAdmin] Appointment ${newAppointmentId} created successfully with status ${initialStatus}.`
|
|
32955
|
+
);
|
|
32956
|
+
return {
|
|
32957
|
+
success: true,
|
|
32958
|
+
appointmentId: newAppointmentId,
|
|
32959
|
+
appointmentData: newAppointmentData
|
|
32960
|
+
};
|
|
32961
|
+
} catch (error) {
|
|
32962
|
+
console.error(
|
|
32963
|
+
"[BookingAdmin] Critical error in orchestrateAppointmentCreation:",
|
|
32964
|
+
error
|
|
32965
|
+
);
|
|
32966
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown server error during appointment creation.";
|
|
32967
|
+
return { success: false, error: errorMessage };
|
|
32717
32968
|
}
|
|
32718
32969
|
}
|
|
32719
32970
|
};
|
|
32720
|
-
|
|
32721
|
-
|
|
32722
|
-
|
|
32723
|
-
const app = container.getProvider("app").getImmediate();
|
|
32724
|
-
const firestoreInstance = new Firestore(new FirebaseAuthCredentialsProvider(container.getProvider("auth-internal")), new FirebaseAppCheckTokenProvider(container.getProvider("app-check-internal")), databaseIdFromApp(app, databaseId), app);
|
|
32725
|
-
settings = Object.assign({ useFetchStreams }, settings);
|
|
32726
|
-
firestoreInstance._setSettings(settings);
|
|
32727
|
-
return firestoreInstance;
|
|
32728
|
-
}, "PUBLIC").setMultipleInstances(true));
|
|
32729
|
-
registerVersion(name2, version$12, variant);
|
|
32730
|
-
registerVersion(name2, version$12, "esm2017");
|
|
32731
|
-
}
|
|
32732
|
-
var FIELD_PATH_RESERVED = new RegExp("[~\\*/\\[\\]]");
|
|
32733
|
-
registerFirestore("node");
|
|
32971
|
+
|
|
32972
|
+
// src/admin/requirements/patient-requirements.admin.service.ts
|
|
32973
|
+
var admin8 = __toESM(require("firebase-admin"));
|
|
32734
32974
|
|
|
32735
32975
|
// src/types/patient/patient-requirements.ts
|
|
32736
32976
|
var PatientInstructionStatus = /* @__PURE__ */ ((PatientInstructionStatus2) => {
|
|
@@ -32823,7 +33063,7 @@ var PatientRequirementsAdminService = class {
|
|
|
32823
33063
|
...currentInstruction,
|
|
32824
33064
|
notificationId: void 0,
|
|
32825
33065
|
status: "cancelled" /* CANCELLED */,
|
|
32826
|
-
updatedAt: new
|
|
33066
|
+
updatedAt: new Timestamp(
|
|
32827
33067
|
adminTsNow.seconds,
|
|
32828
33068
|
adminTsNow.nanoseconds
|
|
32829
33069
|
)
|
|
@@ -32848,7 +33088,7 @@ var PatientRequirementsAdminService = class {
|
|
|
32848
33088
|
currentInstruction = {
|
|
32849
33089
|
...currentInstruction,
|
|
32850
33090
|
notificationId: void 0,
|
|
32851
|
-
updatedAt: new
|
|
33091
|
+
updatedAt: new Timestamp(
|
|
32852
33092
|
adminTsNow.seconds,
|
|
32853
33093
|
adminTsNow.nanoseconds
|
|
32854
33094
|
)
|
|
@@ -32884,7 +33124,7 @@ var PatientRequirementsAdminService = class {
|
|
|
32884
33124
|
...currentInstruction,
|
|
32885
33125
|
notificationId: createdNotificationId,
|
|
32886
33126
|
status: "pendingNotification" /* PENDING_NOTIFICATION */,
|
|
32887
|
-
updatedAt: new
|
|
33127
|
+
updatedAt: new Timestamp(
|
|
32888
33128
|
adminTsNow.seconds,
|
|
32889
33129
|
adminTsNow.nanoseconds
|
|
32890
33130
|
)
|
|
@@ -32908,7 +33148,7 @@ var PatientRequirementsAdminService = class {
|
|
|
32908
33148
|
await instanceDocRef.update({
|
|
32909
33149
|
instructions: updatedInstructions,
|
|
32910
33150
|
// Array of instructions with actual Timestamps
|
|
32911
|
-
updatedAt: new
|
|
33151
|
+
updatedAt: new Timestamp(
|
|
32912
33152
|
finalAdminTsNow.seconds,
|
|
32913
33153
|
finalAdminTsNow.nanoseconds
|
|
32914
33154
|
)
|
|
@@ -32990,7 +33230,7 @@ var PatientRequirementsAdminService = class {
|
|
|
32990
33230
|
currentInstruction = {
|
|
32991
33231
|
...currentInstruction,
|
|
32992
33232
|
status: "missed" /* MISSED */,
|
|
32993
|
-
updatedAt: new
|
|
33233
|
+
updatedAt: new Timestamp(
|
|
32994
33234
|
adminNowForMissed.seconds,
|
|
32995
33235
|
adminNowForMissed.nanoseconds
|
|
32996
33236
|
)
|
|
@@ -33007,7 +33247,7 @@ var PatientRequirementsAdminService = class {
|
|
|
33007
33247
|
await instanceRef.update({
|
|
33008
33248
|
instructions: updatedInstructions,
|
|
33009
33249
|
// Array of instructions with actual Timestamps
|
|
33010
|
-
updatedAt: new
|
|
33250
|
+
updatedAt: new Timestamp(
|
|
33011
33251
|
finalAdminNowForMissedUpdate.seconds,
|
|
33012
33252
|
finalAdminNowForMissedUpdate.nanoseconds
|
|
33013
33253
|
)
|
|
@@ -33051,7 +33291,7 @@ var PatientRequirementsAdminService = class {
|
|
|
33051
33291
|
);
|
|
33052
33292
|
await instanceRef.update({
|
|
33053
33293
|
overallStatus: "active" /* ACTIVE */,
|
|
33054
|
-
updatedAt: new
|
|
33294
|
+
updatedAt: new Timestamp(
|
|
33055
33295
|
admin8.firestore.Timestamp.now().seconds,
|
|
33056
33296
|
admin8.firestore.Timestamp.now().nanoseconds
|
|
33057
33297
|
)
|
|
@@ -33086,7 +33326,7 @@ var PatientRequirementsAdminService = class {
|
|
|
33086
33326
|
const adminTsNow = admin8.firestore.Timestamp.now();
|
|
33087
33327
|
await instanceRef.update({
|
|
33088
33328
|
overallStatus: newOverallStatus,
|
|
33089
|
-
updatedAt: new
|
|
33329
|
+
updatedAt: new Timestamp(
|
|
33090
33330
|
adminTsNow.seconds,
|
|
33091
33331
|
adminTsNow.nanoseconds
|
|
33092
33332
|
)
|