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