|
1 | 1 | // eslint-disable-next-line no-restricted-imports |
2 | | -import { countBy } from "lodash"; |
3 | 2 | import type { Logger } from "tslog"; |
4 | 3 | import { v4 as uuid } from "uuid"; |
5 | 4 |
|
@@ -248,14 +247,21 @@ export const getRegularOrDynamicEventType = withReporting( |
248 | 247 | ); |
249 | 248 |
|
250 | 249 | function applyOccupiedSeatsToCurrentSeats(currentSeats: CurrentSeats, occupiedSeats: SelectedSlots[]) { |
251 | | - const occupiedSeatsCount = countBy(occupiedSeats, (item) => item.slotUtcStartDate.toISOString()); |
252 | | - Object.keys(occupiedSeatsCount).forEach((date) => { |
| 250 | + const occupiedSeatsMap = new Map<string, number>(); |
| 251 | + |
| 252 | + occupiedSeats.forEach((item) => { |
| 253 | + const dateKey = item.slotUtcStartDate.toISOString(); |
| 254 | + occupiedSeatsMap.set(dateKey, (occupiedSeatsMap.get(dateKey) || 0) + 1); |
| 255 | + }); |
| 256 | + |
| 257 | + occupiedSeatsMap.forEach((count, date) => { |
253 | 258 | currentSeats.push({ |
254 | 259 | uid: uuid(), |
255 | 260 | startTime: dayjs(date).toDate(), |
256 | | - _count: { attendees: occupiedSeatsCount[date] }, |
| 261 | + _count: { attendees: count }, |
257 | 262 | }); |
258 | 263 | }); |
| 264 | + |
259 | 265 | return currentSeats; |
260 | 266 | } |
261 | 267 |
|
@@ -542,15 +548,15 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise<I |
542 | 548 |
|
543 | 549 | currentSeats = availabilityCheckProps.currentSeats; |
544 | 550 | } |
| 551 | + const busySlotsFromReservedSlots = reservedSlots.reduce<EventBusyDate[]>((r, c) => { |
| 552 | + if (!c.isSeat) { |
| 553 | + r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); |
| 554 | + } |
| 555 | + return r; |
| 556 | + }, []); |
| 557 | + |
545 | 558 | availableTimeSlots = availableTimeSlots |
546 | 559 | .map((slot) => { |
547 | | - const busySlotsFromReservedSlots = reservedSlots.reduce<EventBusyDate[]>((r, c) => { |
548 | | - if (!c.isSeat) { |
549 | | - r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); |
550 | | - } |
551 | | - return r; |
552 | | - }, []); |
553 | | - |
554 | 560 | if ( |
555 | 561 | !checkForConflicts({ |
556 | 562 | time: slot.time, |
@@ -588,34 +594,41 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise<I |
588 | 594 | }); |
589 | 595 |
|
590 | 596 | function _mapSlotsToDate() { |
| 597 | + const currentSeatsMap = new Map(); |
| 598 | + |
| 599 | + if (currentSeats && currentSeats.length > 0) { |
| 600 | + currentSeats.forEach((booking) => { |
| 601 | + const timeKey = booking.startTime.toISOString(); |
| 602 | + currentSeatsMap.set(timeKey, { |
| 603 | + attendees: booking._count.attendees, |
| 604 | + uid: booking.uid, |
| 605 | + }); |
| 606 | + }); |
| 607 | + } |
| 608 | + |
591 | 609 | return availableTimeSlots.reduce( |
592 | 610 | ( |
593 | 611 | r: Record<string, { time: string; attendees?: number; bookingUid?: string }[]>, |
594 | 612 | { time, ...passThroughProps } |
595 | 613 | ) => { |
596 | | - // TODO: Adds unit tests to prevent regressions in getSchedule (try multiple timezones) |
597 | | - |
598 | 614 | // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. |
599 | 615 | // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. |
600 | 616 | const dateString = formatter.format(time.toDate()); |
| 617 | + const timeISO = time.toISOString(); |
601 | 618 |
|
602 | 619 | r[dateString] = r[dateString] || []; |
603 | 620 | if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) { |
604 | 621 | return r; |
605 | 622 | } |
| 623 | + |
| 624 | + const existingBooking = currentSeatsMap.get(timeISO); |
| 625 | + |
606 | 626 | r[dateString].push({ |
607 | 627 | ...passThroughProps, |
608 | | - time: time.toISOString(), |
609 | | - // Conditionally add the attendees and booking id to slots object if there is already a booking during that time |
610 | | - ...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && { |
611 | | - attendees: |
612 | | - currentSeats[ |
613 | | - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) |
614 | | - ]._count.attendees, |
615 | | - bookingUid: |
616 | | - currentSeats[ |
617 | | - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) |
618 | | - ].uid, |
| 628 | + time: timeISO, |
| 629 | + ...(existingBooking && { |
| 630 | + attendees: existingBooking.attendees, |
| 631 | + bookingUid: existingBooking.uid, |
619 | 632 | }), |
620 | 633 | }); |
621 | 634 | return r; |
@@ -654,35 +667,38 @@ const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise<I |
654 | 667 | function _mapWithinBoundsSlotsToDate() { |
655 | 668 | // This should never happen. Just for type safety, we already check in the upper scope |
656 | 669 | if (!eventType) throw new TRPCError({ code: "NOT_FOUND" }); |
657 | | - return Object.entries(slotsMappedToDate).reduce((withinBoundsSlotsMappedToDate, [date, slots]) => { |
658 | | - // Computation Optimization: If a future limit violation has been found, we just consider all slots to be out of bounds beyond that slot. |
659 | | - // We can't do the same for periodType=RANGE because it can start from a day other than today and today will hit the violation then. |
660 | | - if (foundAFutureLimitViolation && doesRangeStartFromToday(eventType.periodType)) { |
661 | | - return withinBoundsSlotsMappedToDate; |
| 670 | + |
| 671 | + const withinBoundsSlotsMappedToDate = {} as typeof slotsMappedToDate; |
| 672 | + const doesStartFromToday = doesRangeStartFromToday(eventType.periodType); |
| 673 | + |
| 674 | + for (const [date, slots] of Object.entries(slotsMappedToDate)) { |
| 675 | + if (foundAFutureLimitViolation && doesStartFromToday) { |
| 676 | + break; // Instead of continuing the loop, we can break since all future dates will be skipped |
662 | 677 | } |
| 678 | + |
663 | 679 | const filteredSlots = slots.filter((slot) => { |
664 | 680 | const isFutureLimitViolationForTheSlot = isTimeViolatingFutureLimit({ |
665 | 681 | time: slot.time, |
666 | 682 | periodLimits, |
667 | 683 | }); |
| 684 | + |
668 | 685 | if (isFutureLimitViolationForTheSlot) { |
669 | 686 | foundAFutureLimitViolation = true; |
670 | 687 | } |
| 688 | + |
671 | 689 | return ( |
672 | 690 | !isFutureLimitViolationForTheSlot && |
673 | 691 | // TODO: Perf Optimization: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. |
674 | 692 | !isTimeOutOfBounds({ time: slot.time, minimumBookingNotice: eventType.minimumBookingNotice }) |
675 | 693 | ); |
676 | 694 | }); |
677 | 695 |
|
678 | | - if (!filteredSlots.length) { |
679 | | - // If there are no slots available, we don't set that date, otherwise having an empty slots array makes frontend consider it as an all day OOO case |
680 | | - return withinBoundsSlotsMappedToDate; |
| 696 | + if (filteredSlots.length) { |
| 697 | + withinBoundsSlotsMappedToDate[date] = filteredSlots; |
681 | 698 | } |
| 699 | + } |
682 | 700 |
|
683 | | - withinBoundsSlotsMappedToDate[date] = filteredSlots; |
684 | | - return withinBoundsSlotsMappedToDate; |
685 | | - }, {} as typeof slotsMappedToDate); |
| 701 | + return withinBoundsSlotsMappedToDate; |
686 | 702 | } |
687 | 703 | const mapWithinBoundsSlotsToDate = withReporting(_mapWithinBoundsSlotsToDate, "mapWithinBoundsSlotsToDate"); |
688 | 704 | const withinBoundsSlotsMappedToDate = mapWithinBoundsSlotsToDate(); |
|
0 commit comments