From e2618f2c9ad90679de02bf57c1c8c42ffd54c401 Mon Sep 17 00:00:00 2001 From: ikusteu Date: Sun, 10 Sep 2023 14:46:36 +0200 Subject: [PATCH] Account for interval time when sorting customer calendar view (aside from slot date) --- .../bookings/__tests__/bookings.test.ts | 109 +++++++++++++++++- .../src/store/selectors/bookings/slots.ts | 56 +++++---- 2 files changed, 144 insertions(+), 21 deletions(-) diff --git a/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts b/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts index f5e39169a..f1c5139b7 100644 --- a/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts +++ b/packages/client/src/store/selectors/bookings/__tests__/bookings.test.ts @@ -32,7 +32,11 @@ import { slotsByDay, } from "../../__testData__/slots"; import { baseSlot } from "@eisbuk/testing/slots"; -import { getBookingsForCalendar, getMonthEmptyForBooking } from "../slots"; +import { + getBookedAndAttendedSlotsForCalendar, + getBookingsForCalendar, + getMonthEmptyForBooking, +} from "../slots"; // set date mock to be a consistent date throughout const mockDate = DateTime.fromISO("2022-02-05"); @@ -349,5 +353,108 @@ describe("Selectors ->", () => { }, ]); }); + + test.only("should sort the slots (by date, and by time intraday)", () => { + const monthStr = "2022-01"; + const date = "2022-01-01"; + const tomorrow = "2022-01-02"; + + const intervals = { + "09:00-10:00": { + startTime: "09:00", + endTime: "10:00", + }, + "10:00-11:00": { + startTime: "10:00", + endTime: "11:00", + }, + "11:00-12:00": { + startTime: "11:00", + endTime: "12:00", + }, + }; + + const [slot1, slot2, slot3, slot4] = [ + { + ...baseSlot, + id: "slot-1", + intervals, + date, + categories: [Category.Competitive], + }, + { + ...baseSlot, + id: "slot-2", + intervals, + date, + categories: [Category.Competitive], + }, + { + ...baseSlot, + id: "slot-3", + intervals, + date, + categories: [Category.Competitive], + }, + { + ...baseSlot, + id: "slot-4", + intervals, + date: tomorrow, + categories: [Category.Competitive], + }, + ]; + + const store = setupBookingsTest({ + category: Category.Competitive, + // Any day from the test month would do + date: DateTime.fromISO(date), + slotsByDay: { + [monthStr]: { + // We're adding slots in an arbitrary order to intrduce additional entropy before sorting + [date]: { [slot3.id]: slot3, [slot2.id]: slot2, [slot1.id]: slot1 }, + [tomorrow]: { [slot4.id]: slot4 }, + }, + }, + }); + + store.dispatch( + // We're booking the slots in the same order as they appear in the store, + // this should fail the test unless the fix (tested by this) is not in place. + updateLocalDocuments(BookingSubCollection.BookedSlots, { + [slot3.id]: { + date: slot3.date, + // Last interval (should appear last of all the slots in the day) + interval: "11:00-12:00", + }, + [slot1.id]: { + date: slot1.date, + // First interval (should appear first, regardless of the order it's been added) + interval: "09:00-10:00", + }, + [slot4.id]: { + date: slot4.date, + // Regerdless of this not being the last interval, this slot should appear last as the date sorting takes precenence + interval: "09:00-10:00", + }, + }) + ); + // Second slot should be attended, not booked to verify that the final result sorts all results, regerdless of the booked state + store.dispatch( + updateLocalDocuments(BookingSubCollection.AttendedSlots, { + [slot2.id]: { + date: slot2.date, + // Second interval (we want this slot to appear 2nd - be merged with the booked slots, and then sorted) + interval: "10:00-11:00", + }, + }) + ); + + // It's enough to just check that the ids appear in the desired order + const ids = getBookedAndAttendedSlotsForCalendar(store.getState()).map( + ({ id }) => id + ); + expect(ids).toEqual(["slot-1", "slot-2", "slot-3", "slot-4"]); + }); }); }); diff --git a/packages/client/src/store/selectors/bookings/slots.ts b/packages/client/src/store/selectors/bookings/slots.ts index 40b343d74..a8530b401 100644 --- a/packages/client/src/store/selectors/bookings/slots.ts +++ b/packages/client/src/store/selectors/bookings/slots.ts @@ -133,14 +133,13 @@ export const getMonthEmptyForBooking = (state: LocalStore): boolean => { return isEmpty(getSlotsForCustomer(state)); }; -type BookingsList = Array; -type BookedAndAttendedList = Array< - SlotInterface & { interval: SlotInterval } & { booked: boolean } ->; +type BookingsEntry = SlotInterface & { + interval: SlotInterval; + booked: true; +}; +type BookingsList = Array; -export const getBookingsForCalendar = ( - state: LocalStore -): (SlotInterface & { interval: SlotInterval })[] => { +export const getBookingsForCalendar = (state: LocalStore): BookingsList => { // Current month in view is determined by `currentDate` in Redux store const monthString = getCalendarDay(state).toISO().substring(0, 7); // Get all booked slots @@ -150,17 +149,21 @@ export const getBookingsForCalendar = ( const slotsForAMonth = slotsByMonth[monthString] || {}; return Object.entries(bookedSlots) + .filter(([, { date }]) => Boolean(slotsForAMonth[date])) .reduce( (acc, [slotId, { date, interval: bookedInterval, bookingNotes }]) => { // If this returns undefined, our slot isn't in date range - const dayOfBookedSlot = slotsForAMonth[date]; - if (!dayOfBookedSlot) { - return acc; - } - const bookedSlot = dayOfBookedSlot[slotId]; + const bookedSlot = slotsForAMonth[date][slotId]; const interval = bookedSlot.intervals[bookedInterval]; - const completeBookingEntry = { ...bookedSlot, interval, bookingNotes }; - return [...acc, completeBookingEntry]; + return [ + ...acc, + { + ...bookedSlot, + interval, + bookingNotes, + booked: true, + } as BookingsEntry, + ]; }, [] as BookingsList ) @@ -170,9 +173,15 @@ export const getBookingsForCalendar = ( export const getHasBookingsForCalendar = (state: LocalStore): boolean => Boolean(getBookingsForCalendar(state).length); +type CalendarSlotEntry = SlotInterface & { + interval: SlotInterval; + booked: boolean; +}; +type CalendarSlotList = CalendarSlotEntry[]; + export const getBookedAndAttendedSlotsForCalendar = ( state: LocalStore -): (SlotInterface & { interval: SlotInterval } & { booked: boolean })[] => { +): CalendarSlotList => { // Current month in view is determined by `currentDate` in Redux store const monthString = getCalendarDay(state).toISO().substring(0, 7); @@ -200,7 +209,7 @@ export const getBookedAndAttendedSlotsForCalendar = ( }; return [...acc, completeAttendanceEntry]; }, - [] as BookedAndAttendedList + [] as CalendarSlotList ); const bookedSlotsObj = Object.entries(bookedSlots).reduce( (acc, [slotId, { date, interval: bookedInterval, bookingNotes }]) => { @@ -220,9 +229,16 @@ export const getBookedAndAttendedSlotsForCalendar = ( }; return [...acc, completeBookingEntry]; }, - [] as BookedAndAttendedList - ); - return [...attendedSlotsObj, ...bookedSlotsObj].sort((a, b) => - a.date < b.date ? -1 : 1 + [] as CalendarSlotList ); + return [...attendedSlotsObj, ...bookedSlotsObj].sort(sortCalendarSlots); }; + +const sortCalendarSlots = (a: CalendarSlotEntry, b: CalendarSlotEntry) => + a.date < b.date + ? -1 + : a.date > b.date + ? 1 + : a.interval.startTime < b.interval.startTime + ? -1 + : 1;