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