@blackcode_sa/metaestetics-api 1.5.32 → 1.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,686 @@
1
+ import { Timestamp } from "firebase/firestore";
2
+ import {
3
+ BookingAvailabilityRequest,
4
+ BookingAvailabilityResponse,
5
+ AvailableSlot,
6
+ TimeInterval,
7
+ } from "./booking.types";
8
+ import {
9
+ CalendarEvent,
10
+ CalendarEventStatus,
11
+ CalendarEventType,
12
+ } from "../../types/calendar";
13
+ import { PractitionerClinicWorkingHours } from "../../types/practitioner";
14
+ import { Clinic } from "../../types/clinic";
15
+
16
+ /**
17
+ * Calculator for determining available booking slots
18
+ * This class handles the complex logic of determining when appointments can be scheduled
19
+ * based on clinic working hours, practitioner availability, and existing calendar events.
20
+ */
21
+ export class BookingAvailabilityCalculator {
22
+ /** Default scheduling interval in minutes if not specified by the clinic */
23
+ private static readonly DEFAULT_INTERVAL_MINUTES = 15;
24
+
25
+ /**
26
+ * Calculate available booking slots based on the provided data
27
+ *
28
+ * @param request - The request containing all necessary data for calculation
29
+ * @returns Response with available booking slots
30
+ */
31
+ public static calculateSlots(
32
+ request: BookingAvailabilityRequest
33
+ ): BookingAvailabilityResponse {
34
+ // Extract necessary data from the request
35
+ const {
36
+ clinic,
37
+ practitioner,
38
+ procedure,
39
+ timeframe,
40
+ clinicCalendarEvents,
41
+ practitionerCalendarEvents,
42
+ } = request;
43
+
44
+ // Get scheduling interval (default to 15 minutes if not specified)
45
+ const schedulingIntervalMinutes =
46
+ (clinic as any).schedulingInterval || this.DEFAULT_INTERVAL_MINUTES;
47
+
48
+ // Get procedure duration in minutes
49
+ const procedureDurationMinutes = procedure.duration;
50
+
51
+ console.log(
52
+ `Calculating slots with interval: ${schedulingIntervalMinutes}min and procedure duration: ${procedureDurationMinutes}min`
53
+ );
54
+
55
+ // Start with the full timeframe as initially available
56
+ let availableIntervals: TimeInterval[] = [
57
+ { start: timeframe.start, end: timeframe.end },
58
+ ];
59
+
60
+ // Step 1: Apply clinic working hours
61
+ availableIntervals = this.applyClinicWorkingHours(
62
+ availableIntervals,
63
+ clinic.workingHours,
64
+ timeframe
65
+ );
66
+
67
+ // Step 2: Subtract clinic blocking events
68
+ availableIntervals = this.subtractBlockingEvents(
69
+ availableIntervals,
70
+ clinicCalendarEvents
71
+ );
72
+
73
+ // Step 3: Apply practitioner's working hours for this clinic
74
+ availableIntervals = this.applyPractitionerWorkingHours(
75
+ availableIntervals,
76
+ practitioner,
77
+ clinic.id,
78
+ timeframe
79
+ );
80
+
81
+ // Step 4: Subtract practitioner's busy times
82
+ availableIntervals = this.subtractPractitionerBusyTimes(
83
+ availableIntervals,
84
+ practitionerCalendarEvents
85
+ );
86
+
87
+ console.log(
88
+ `After all filters, have ${availableIntervals.length} available intervals`
89
+ );
90
+
91
+ // Step 5: Generate available slots based on scheduling interval and procedure duration
92
+ const availableSlots = this.generateAvailableSlots(
93
+ availableIntervals,
94
+ schedulingIntervalMinutes,
95
+ procedureDurationMinutes
96
+ );
97
+
98
+ return { availableSlots };
99
+ }
100
+
101
+ /**
102
+ * Apply clinic working hours to available intervals
103
+ *
104
+ * @param intervals - Current available intervals
105
+ * @param workingHours - Clinic working hours
106
+ * @param timeframe - Overall timeframe being considered
107
+ * @returns Intervals filtered by clinic working hours
108
+ */
109
+ private static applyClinicWorkingHours(
110
+ intervals: TimeInterval[],
111
+ workingHours: any, // Using 'any' for now since we're working with the existing type structure
112
+ timeframe: { start: Timestamp; end: Timestamp }
113
+ ): TimeInterval[] {
114
+ if (!intervals.length) return [];
115
+ console.log(
116
+ `Applying clinic working hours to ${intervals.length} intervals`
117
+ );
118
+
119
+ // Create working intervals for each day in the timeframe based on clinic's working hours
120
+ const workingIntervals = this.createWorkingHoursIntervals(
121
+ workingHours,
122
+ timeframe.start.toDate(),
123
+ timeframe.end.toDate()
124
+ );
125
+
126
+ // Intersect the available intervals with working hours intervals
127
+ return this.intersectIntervals(intervals, workingIntervals);
128
+ }
129
+
130
+ /**
131
+ * Create time intervals for working hours across multiple days
132
+ *
133
+ * @param workingHours - Working hours definition
134
+ * @param startDate - Start date of the overall timeframe
135
+ * @param endDate - End date of the overall timeframe
136
+ * @returns Array of time intervals representing working hours
137
+ */
138
+ private static createWorkingHoursIntervals(
139
+ workingHours: any,
140
+ startDate: Date,
141
+ endDate: Date
142
+ ): TimeInterval[] {
143
+ const workingIntervals: TimeInterval[] = [];
144
+
145
+ // Clone the start date to avoid modifying the original
146
+ const currentDate = new Date(startDate);
147
+
148
+ // Reset time to start of day
149
+ currentDate.setHours(0, 0, 0, 0);
150
+
151
+ // Create a map of day names to day numbers (0 = Sunday, 1 = Monday, etc.)
152
+ const dayNameToNumber: { [key: string]: number } = {
153
+ sunday: 0,
154
+ monday: 1,
155
+ tuesday: 2,
156
+ wednesday: 3,
157
+ thursday: 4,
158
+ friday: 5,
159
+ saturday: 6,
160
+ };
161
+
162
+ // Iterate through each day in the timeframe
163
+ while (currentDate <= endDate) {
164
+ const dayOfWeek = currentDate.getDay();
165
+ const dayName = Object.keys(dayNameToNumber).find(
166
+ (key) => dayNameToNumber[key] === dayOfWeek
167
+ );
168
+
169
+ if (dayName && workingHours[dayName]) {
170
+ const daySchedule = workingHours[dayName];
171
+
172
+ if (daySchedule) {
173
+ // Parse open and close times
174
+ const [openHours, openMinutes] = daySchedule.open
175
+ .split(":")
176
+ .map(Number);
177
+ const [closeHours, closeMinutes] = daySchedule.close
178
+ .split(":")
179
+ .map(Number);
180
+
181
+ // Create start and end times for this working day
182
+ const workStart = new Date(currentDate);
183
+ workStart.setHours(openHours, openMinutes, 0, 0);
184
+
185
+ const workEnd = new Date(currentDate);
186
+ workEnd.setHours(closeHours, closeMinutes, 0, 0);
187
+
188
+ // Only add if the working hours are within our timeframe
189
+ if (workEnd > startDate && workStart < endDate) {
190
+ // Adjust interval to be within our overall timeframe
191
+ const intervalStart = workStart < startDate ? startDate : workStart;
192
+ const intervalEnd = workEnd > endDate ? endDate : workEnd;
193
+
194
+ workingIntervals.push({
195
+ start: Timestamp.fromDate(intervalStart),
196
+ end: Timestamp.fromDate(intervalEnd),
197
+ });
198
+
199
+ // Handle breaks if they exist
200
+ if (daySchedule.breaks && daySchedule.breaks.length > 0) {
201
+ for (const breakTime of daySchedule.breaks) {
202
+ const [breakStartHours, breakStartMinutes] = breakTime.start
203
+ .split(":")
204
+ .map(Number);
205
+ const [breakEndHours, breakEndMinutes] = breakTime.end
206
+ .split(":")
207
+ .map(Number);
208
+
209
+ const breakStart = new Date(currentDate);
210
+ breakStart.setHours(breakStartHours, breakStartMinutes, 0, 0);
211
+
212
+ const breakEnd = new Date(currentDate);
213
+ breakEnd.setHours(breakEndHours, breakEndMinutes, 0, 0);
214
+
215
+ // Subtract this break from our working intervals
216
+ workingIntervals.splice(
217
+ -1,
218
+ 1,
219
+ ...this.subtractInterval(
220
+ workingIntervals[workingIntervals.length - 1],
221
+ {
222
+ start: Timestamp.fromDate(breakStart),
223
+ end: Timestamp.fromDate(breakEnd),
224
+ }
225
+ )
226
+ );
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // Move to the next day
234
+ currentDate.setDate(currentDate.getDate() + 1);
235
+ }
236
+
237
+ return workingIntervals;
238
+ }
239
+
240
+ /**
241
+ * Subtract blocking events from available intervals
242
+ *
243
+ * @param intervals - Current available intervals
244
+ * @param events - Calendar events to subtract
245
+ * @returns Available intervals after removing blocking events
246
+ */
247
+ private static subtractBlockingEvents(
248
+ intervals: TimeInterval[],
249
+ events: CalendarEvent[]
250
+ ): TimeInterval[] {
251
+ if (!intervals.length) return [];
252
+ console.log(`Subtracting ${events.length} blocking events`);
253
+
254
+ // Filter only blocking-type events
255
+ const blockingEvents = events.filter(
256
+ (event) =>
257
+ event.eventType === CalendarEventType.BLOCKING ||
258
+ event.eventType === CalendarEventType.BREAK ||
259
+ event.eventType === CalendarEventType.FREE_DAY
260
+ );
261
+
262
+ let result = [...intervals];
263
+
264
+ // For each blocking event, subtract its time from the available intervals
265
+ for (const event of blockingEvents) {
266
+ const { start, end } = event.eventTime;
267
+ const blockingInterval = { start, end };
268
+
269
+ // Create a new result array by subtracting the blocking interval from each available interval
270
+ const newResult: TimeInterval[] = [];
271
+
272
+ for (const interval of result) {
273
+ const remainingIntervals = this.subtractInterval(
274
+ interval,
275
+ blockingInterval
276
+ );
277
+ newResult.push(...remainingIntervals);
278
+ }
279
+
280
+ result = newResult;
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ /**
287
+ * Apply practitioner's specific working hours for the given clinic
288
+ *
289
+ * @param intervals - Current available intervals
290
+ * @param practitioner - Practitioner object
291
+ * @param clinicId - ID of the clinic
292
+ * @param timeframe - Overall timeframe being considered
293
+ * @returns Intervals filtered by practitioner's working hours
294
+ */
295
+ private static applyPractitionerWorkingHours(
296
+ intervals: TimeInterval[],
297
+ practitioner: any,
298
+ clinicId: string,
299
+ timeframe: { start: Timestamp; end: Timestamp }
300
+ ): TimeInterval[] {
301
+ if (!intervals.length) return [];
302
+ console.log(`Applying practitioner working hours for clinic ${clinicId}`);
303
+
304
+ // Find practitioner's working hours for this specific clinic
305
+ const clinicWorkingHours = practitioner.clinicWorkingHours.find(
306
+ (hours: PractitionerClinicWorkingHours) =>
307
+ hours.clinicId === clinicId && hours.isActive
308
+ );
309
+
310
+ // If no specific working hours are found, practitioner isn't available at this clinic
311
+ if (!clinicWorkingHours) {
312
+ console.log(
313
+ `No working hours found for practitioner at clinic ${clinicId}`
314
+ );
315
+ return [];
316
+ }
317
+
318
+ // Create working intervals for each day in the timeframe based on practitioner's working hours
319
+ const workingIntervals = this.createPractitionerWorkingHoursIntervals(
320
+ clinicWorkingHours.workingHours,
321
+ timeframe.start.toDate(),
322
+ timeframe.end.toDate()
323
+ );
324
+
325
+ // Intersect the available intervals with practitioner's working hours intervals
326
+ return this.intersectIntervals(intervals, workingIntervals);
327
+ }
328
+
329
+ /**
330
+ * Create time intervals for practitioner's working hours across multiple days
331
+ *
332
+ * @param workingHours - Practitioner's working hours definition
333
+ * @param startDate - Start date of the overall timeframe
334
+ * @param endDate - End date of the overall timeframe
335
+ * @returns Array of time intervals representing practitioner's working hours
336
+ */
337
+ private static createPractitionerWorkingHoursIntervals(
338
+ workingHours: any,
339
+ startDate: Date,
340
+ endDate: Date
341
+ ): TimeInterval[] {
342
+ const workingIntervals: TimeInterval[] = [];
343
+
344
+ // Clone the start date to avoid modifying the original
345
+ const currentDate = new Date(startDate);
346
+
347
+ // Reset time to start of day
348
+ currentDate.setHours(0, 0, 0, 0);
349
+
350
+ // Create a map of day names to day numbers (0 = Sunday, 1 = Monday, etc.)
351
+ const dayNameToNumber: { [key: string]: number } = {
352
+ sunday: 0,
353
+ monday: 1,
354
+ tuesday: 2,
355
+ wednesday: 3,
356
+ thursday: 4,
357
+ friday: 5,
358
+ saturday: 6,
359
+ };
360
+
361
+ // Iterate through each day in the timeframe
362
+ while (currentDate <= endDate) {
363
+ const dayOfWeek = currentDate.getDay();
364
+ const dayName = Object.keys(dayNameToNumber).find(
365
+ (key) => dayNameToNumber[key] === dayOfWeek
366
+ );
367
+
368
+ if (dayName && workingHours[dayName]) {
369
+ const daySchedule = workingHours[dayName];
370
+
371
+ if (daySchedule) {
372
+ // Parse start and end times
373
+ const [startHours, startMinutes] = daySchedule.start
374
+ .split(":")
375
+ .map(Number);
376
+ const [endHours, endMinutes] = daySchedule.end.split(":").map(Number);
377
+
378
+ // Create start and end times for this working day
379
+ const workStart = new Date(currentDate);
380
+ workStart.setHours(startHours, startMinutes, 0, 0);
381
+
382
+ const workEnd = new Date(currentDate);
383
+ workEnd.setHours(endHours, endMinutes, 0, 0);
384
+
385
+ // Only add if the working hours are within our timeframe
386
+ if (workEnd > startDate && workStart < endDate) {
387
+ // Adjust interval to be within our overall timeframe
388
+ const intervalStart = workStart < startDate ? startDate : workStart;
389
+ const intervalEnd = workEnd > endDate ? endDate : workEnd;
390
+
391
+ workingIntervals.push({
392
+ start: Timestamp.fromDate(intervalStart),
393
+ end: Timestamp.fromDate(intervalEnd),
394
+ });
395
+ }
396
+ }
397
+ }
398
+
399
+ // Move to the next day
400
+ currentDate.setDate(currentDate.getDate() + 1);
401
+ }
402
+
403
+ return workingIntervals;
404
+ }
405
+
406
+ /**
407
+ * Subtract practitioner's busy times from available intervals
408
+ *
409
+ * @param intervals - Current available intervals
410
+ * @param events - Practitioner's calendar events
411
+ * @returns Available intervals after removing busy times
412
+ */
413
+ private static subtractPractitionerBusyTimes(
414
+ intervals: TimeInterval[],
415
+ events: CalendarEvent[]
416
+ ): TimeInterval[] {
417
+ if (!intervals.length) return [];
418
+ console.log(`Subtracting ${events.length} practitioner events`);
419
+
420
+ // Filter events that make the practitioner busy (pending, confirmed, blocking)
421
+ const busyEvents = events.filter(
422
+ (event) =>
423
+ // Include all blocking events
424
+ event.eventType === CalendarEventType.BLOCKING ||
425
+ event.eventType === CalendarEventType.BREAK ||
426
+ event.eventType === CalendarEventType.FREE_DAY ||
427
+ // Include appointments that are pending, confirmed, or rescheduled
428
+ (event.eventType === CalendarEventType.APPOINTMENT &&
429
+ (event.status === CalendarEventStatus.PENDING ||
430
+ event.status === CalendarEventStatus.CONFIRMED ||
431
+ event.status === CalendarEventStatus.RESCHEDULED))
432
+ );
433
+
434
+ let result = [...intervals];
435
+
436
+ // For each busy event, subtract its time from the available intervals
437
+ for (const event of busyEvents) {
438
+ const { start, end } = event.eventTime;
439
+ const busyInterval = { start, end };
440
+
441
+ // Create a new result array by subtracting the busy interval from each available interval
442
+ const newResult: TimeInterval[] = [];
443
+
444
+ for (const interval of result) {
445
+ const remainingIntervals = this.subtractInterval(
446
+ interval,
447
+ busyInterval
448
+ );
449
+ newResult.push(...remainingIntervals);
450
+ }
451
+
452
+ result = newResult;
453
+ }
454
+
455
+ return result;
456
+ }
457
+
458
+ /**
459
+ * Generate available booking slots based on the final available intervals
460
+ *
461
+ * @param intervals - Final available intervals
462
+ * @param intervalMinutes - Scheduling interval in minutes
463
+ * @param durationMinutes - Procedure duration in minutes
464
+ * @returns Array of available booking slots
465
+ */
466
+ private static generateAvailableSlots(
467
+ intervals: TimeInterval[],
468
+ intervalMinutes: number,
469
+ durationMinutes: number
470
+ ): AvailableSlot[] {
471
+ const slots: AvailableSlot[] = [];
472
+ console.log(
473
+ `Generating slots with ${intervalMinutes}min intervals for ${durationMinutes}min procedure`
474
+ );
475
+
476
+ // Convert duration to milliseconds
477
+ const durationMs = durationMinutes * 60 * 1000;
478
+ // Convert interval to milliseconds
479
+ const intervalMs = intervalMinutes * 60 * 1000;
480
+
481
+ // For each available interval
482
+ for (const interval of intervals) {
483
+ // Convert timestamps to JS Date objects for easier manipulation
484
+ const intervalStart = interval.start.toDate();
485
+ const intervalEnd = interval.end.toDate();
486
+
487
+ // Start at the beginning of the interval
488
+ let slotStart = new Date(intervalStart);
489
+
490
+ // Adjust slotStart to the nearest interval boundary if needed
491
+ const minutesIntoDay = slotStart.getHours() * 60 + slotStart.getMinutes();
492
+ const minutesRemainder = minutesIntoDay % intervalMinutes;
493
+
494
+ if (minutesRemainder > 0) {
495
+ slotStart.setMinutes(
496
+ slotStart.getMinutes() + (intervalMinutes - minutesRemainder)
497
+ );
498
+ }
499
+
500
+ // Iterate through potential start times
501
+ while (slotStart.getTime() + durationMs <= intervalEnd.getTime()) {
502
+ // Calculate potential end time
503
+ const slotEnd = new Date(slotStart.getTime() + durationMs);
504
+
505
+ // Check if this slot fits entirely within one of our available intervals
506
+ if (this.isSlotFullyAvailable(slotStart, slotEnd, intervals)) {
507
+ slots.push({
508
+ start: Timestamp.fromDate(slotStart),
509
+ });
510
+ }
511
+
512
+ // Move to the next potential start time
513
+ slotStart = new Date(slotStart.getTime() + intervalMs);
514
+ }
515
+ }
516
+
517
+ console.log(`Generated ${slots.length} available slots`);
518
+ return slots;
519
+ }
520
+
521
+ /**
522
+ * Check if a time slot is fully available within the given intervals
523
+ *
524
+ * @param slotStart - Start time of the slot
525
+ * @param slotEnd - End time of the slot
526
+ * @param intervals - Available intervals
527
+ * @returns True if the slot is fully contained within an available interval
528
+ */
529
+ private static isSlotFullyAvailable(
530
+ slotStart: Date,
531
+ slotEnd: Date,
532
+ intervals: TimeInterval[]
533
+ ): boolean {
534
+ // Check if the slot is fully contained in any of the available intervals
535
+ return intervals.some((interval) => {
536
+ const intervalStart = interval.start.toDate();
537
+ const intervalEnd = interval.end.toDate();
538
+
539
+ return slotStart >= intervalStart && slotEnd <= intervalEnd;
540
+ });
541
+ }
542
+
543
+ /**
544
+ * Intersect two sets of time intervals
545
+ *
546
+ * @param intervalsA - First set of intervals
547
+ * @param intervalsB - Second set of intervals
548
+ * @returns Intersection of the two sets of intervals
549
+ */
550
+ private static intersectIntervals(
551
+ intervalsA: TimeInterval[],
552
+ intervalsB: TimeInterval[]
553
+ ): TimeInterval[] {
554
+ const result: TimeInterval[] = [];
555
+
556
+ // For each pair of intervals, find their intersection
557
+ for (const intervalA of intervalsA) {
558
+ for (const intervalB of intervalsB) {
559
+ // Find the later of the two start times
560
+ const intersectionStart =
561
+ intervalA.start.toMillis() > intervalB.start.toMillis()
562
+ ? intervalA.start
563
+ : intervalB.start;
564
+
565
+ // Find the earlier of the two end times
566
+ const intersectionEnd =
567
+ intervalA.end.toMillis() < intervalB.end.toMillis()
568
+ ? intervalA.end
569
+ : intervalB.end;
570
+
571
+ // If there is a valid intersection, add it to the result
572
+ if (intersectionStart.toMillis() < intersectionEnd.toMillis()) {
573
+ result.push({
574
+ start: intersectionStart,
575
+ end: intersectionEnd,
576
+ });
577
+ }
578
+ }
579
+ }
580
+
581
+ return this.mergeOverlappingIntervals(result);
582
+ }
583
+
584
+ /**
585
+ * Subtract one interval from another, potentially resulting in 0, 1, or 2 intervals
586
+ *
587
+ * @param interval - Interval to subtract from
588
+ * @param subtrahend - Interval to subtract
589
+ * @returns Array of remaining intervals after subtraction
590
+ */
591
+ private static subtractInterval(
592
+ interval: TimeInterval,
593
+ subtrahend: TimeInterval
594
+ ): TimeInterval[] {
595
+ // Case 1: No overlap - return the original interval
596
+ if (
597
+ interval.end.toMillis() <= subtrahend.start.toMillis() ||
598
+ interval.start.toMillis() >= subtrahend.end.toMillis()
599
+ ) {
600
+ return [interval];
601
+ }
602
+
603
+ // Case 2: Subtrahend covers the entire interval - return empty array
604
+ if (
605
+ subtrahend.start.toMillis() <= interval.start.toMillis() &&
606
+ subtrahend.end.toMillis() >= interval.end.toMillis()
607
+ ) {
608
+ return [];
609
+ }
610
+
611
+ // Case 3: Subtrahend splits the interval - return two intervals
612
+ if (
613
+ subtrahend.start.toMillis() > interval.start.toMillis() &&
614
+ subtrahend.end.toMillis() < interval.end.toMillis()
615
+ ) {
616
+ return [
617
+ {
618
+ start: interval.start,
619
+ end: subtrahend.start,
620
+ },
621
+ {
622
+ start: subtrahend.end,
623
+ end: interval.end,
624
+ },
625
+ ];
626
+ }
627
+
628
+ // Case 4: Subtrahend overlaps only the start - return the remaining end portion
629
+ if (
630
+ subtrahend.start.toMillis() <= interval.start.toMillis() &&
631
+ subtrahend.end.toMillis() > interval.start.toMillis()
632
+ ) {
633
+ return [
634
+ {
635
+ start: subtrahend.end,
636
+ end: interval.end,
637
+ },
638
+ ];
639
+ }
640
+
641
+ // Case 5: Subtrahend overlaps only the end - return the remaining start portion
642
+ return [
643
+ {
644
+ start: interval.start,
645
+ end: subtrahend.start,
646
+ },
647
+ ];
648
+ }
649
+
650
+ /**
651
+ * Merge overlapping intervals to simplify the result
652
+ *
653
+ * @param intervals - Intervals to merge
654
+ * @returns Merged intervals
655
+ */
656
+ private static mergeOverlappingIntervals(
657
+ intervals: TimeInterval[]
658
+ ): TimeInterval[] {
659
+ if (intervals.length <= 1) return intervals;
660
+
661
+ // Sort intervals by start time
662
+ const sorted = [...intervals].sort(
663
+ (a, b) => a.start.toMillis() - b.start.toMillis()
664
+ );
665
+
666
+ const result: TimeInterval[] = [sorted[0]];
667
+
668
+ for (let i = 1; i < sorted.length; i++) {
669
+ const current = sorted[i];
670
+ const lastResult = result[result.length - 1];
671
+
672
+ // If current interval overlaps with the last result interval, merge them
673
+ if (current.start.toMillis() <= lastResult.end.toMillis()) {
674
+ // Update the end time of the last result to be the maximum of the two end times
675
+ if (current.end.toMillis() > lastResult.end.toMillis()) {
676
+ lastResult.end = current.end;
677
+ }
678
+ } else {
679
+ // No overlap, add the current interval to the result
680
+ result.push(current);
681
+ }
682
+ }
683
+
684
+ return result;
685
+ }
686
+ }