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