import moment from 'moment' import { prismaClient as prisma } from '../../prismaClient' import { groupBy } from 'lodash' import { getUsersForSegments } from '../segment' const convertHoursToMS = (hours) => hours * 1000 * 60 * 60 const convertMinutesToMS = (minutes) => minutes * 1000 * 60 const ONE_HOUR_MS = 1000 * 60 * 60 const TWENTY_FOUR_HOURS_MS = ONE_HOUR_MS * 24 type BookingType = { start: Date durationMS: number numberOfResidents: number } type GetTimeSlotsInputType = { openAtMS: number closeAtMS: number usageDurationMS: number allowMultipleBookings: boolean maximumUserAtSameTime: number timeBetweenSlotMS: number timeslotIncrementalMS: number existingBookings: BookingType[] } type TimeSlot = { noMoreBookingAvailable: boolean startMS: number endMS: number displayText: string currentUserBookedThisTimeSlot: boolean } export const getTimeSlots = ({ openAtMS, closeAtMS, usageDurationMS, allowMultipleBookings, maximumUserAtSameTime, timeBetweenSlotMS, timeslotIncrementalMS, existingBookings, }: GetTimeSlotsInputType): TimeSlot[] => { const slots = [] const dayStartAtMS = openAtMS || 0 const dayCloseAtMS = closeAtMS || TWENTY_FOUR_HOURS_MS const timeNeededForSlotMS = usageDurationMS let currentTimeMS = dayStartAtMS // While we have some place to create new slots while (currentTimeMS < dayCloseAtMS) { const existingBookingsForThatTime = existingBookings.filter((booking) => { const start = moment(booking.start) const startHourMS = convertHoursToMS(start.hour()) const startMinutesMS = convertMinutesToMS(start.minutes()) const bookingStartMS = startHourMS + startMinutesMS const bookingEndMS = bookingStartMS + booking.durationMS if (currentTimeMS <= bookingStartMS) { return currentTimeMS + timeNeededForSlotMS > bookingStartMS } else { return currentTimeMS > bookingStartMS && currentTimeMS < bookingEndMS } }) const numberOfexistingBookingsForThatTimeResidents = existingBookingsForThatTime.reduce( (acc, booking) => (acc += booking.numberOfResidents || 1), 0 ) let slotIsDisabled = false // Might be valid if we allowed multiple bookings for the same time slot if (allowMultipleBookings) { if (maximumUserAtSameTime) { slotIsDisabled = numberOfexistingBookingsForThatTimeResidents >= maximumUserAtSameTime } } else { slotIsDisabled = numberOfexistingBookingsForThatTimeResidents > 0 } // Check for time between slot for clean-up // If we only check with existing booking it won't work // Because let's say 2 people can book at same time // We still need to block the x hour clean-up after the booking if (timeBetweenSlotMS) { if (!slotIsDisabled) { const existingBooking = existingBookings.some((booking) => { const start = moment(booking.start) const startHourMS = convertHoursToMS(start.hour()) const startMinutesMS = convertMinutesToMS(start.minutes()) const bookingStartMS = startHourMS + startMinutesMS const bookingEndMS = bookingStartMS + booking.durationMS return currentTimeMS >= bookingEndMS && currentTimeMS < bookingEndMS + timeBetweenSlotMS }) if (existingBooking) { slotIsDisabled = true } } } // Maybe we can't create anymore slot if (currentTimeMS + timeNeededForSlotMS <= dayCloseAtMS) { const timeDisplayStart = moment.utc(currentTimeMS).format('HH:mm') const timeDisplayEnd = moment.utc(currentTimeMS + usageDurationMS).format('HH:mm') slots.push({ noMoreBookingAvailable: slotIsDisabled, startMS: currentTimeMS, endMS: currentTimeMS + usageDurationMS, displayText: `${timeDisplayStart} - ${timeDisplayEnd}`, currentUserBookedThisTimeSlot: false, // TODO: need to put logic here to say if the currentUser already booked }) } // Go to the next time slot currentTimeMS += timeslotIncrementalMS } return slots } export async function getUnavaibleDatesForUser({ userId, amenityId, projectId }): Promise { const user = await prisma.user.findOne({ where: { id: userId } }) const amenity = await prisma.amenity.findOne({ where: { id: amenityId }, select: { bookingOptions: { select: { projectId: true, segments: { select: { id: true, }, }, }, }, }, }) let bookingOption if (amenity?.bookingOptions?.length) { // Take only the bookingOption of project because we might have created multiple booking options for different project // if for example the amenity is shared between multiple projects. let bookingOptionsForProject = amenity.bookingOptions.filter( (bookingOption) => bookingOption?.projectId === projectId ) // If we can't find any specified projects we just take the first one... if (bookingOptionsForProject.length === 0) { bookingOptionsForProject = amenity.bookingOptions } // We can then specifiy some segment associatied with booking option // So make sure the user that wants to book get the right booking option based // on in what group he/she is. await Promise.all( bookingOptionsForProject.map(async (bO) => { if (bO.segments.length > 0) { const users = await getUsersForSegments(bO.segments) if (users.some(({ id }) => id === user.id)) { bookingOption = bO } } }) ) // Otherwise, take the one without any segment which is the default one if (!bookingOption) { bookingOption = bookingOptionsForProject.find( (bookingOption) => bookingOption.segments.length === 0 ) } } const existingReservationsForAmenity = await prisma.reservation.findMany({ where: { amenity: { id: { equals: amenityId, }, }, }, }) // By default, if there's no booking option, we have decided that it's 1 booking max per day // PM should specifiy booking option but if they forgot we at least have a default system const maxNumberOfBookingAtSameTime = !bookingOption ? 1 : bookingOption.maximumBookingsAtSameTime || 1 const existingReservationsForAmenityDates = existingReservationsForAmenity.map(({ start }) => moment(start).format('YYYY/MM/DD') ) const bookingDatesGrouped = groupBy(existingReservationsForAmenityDates, 'length') const disabledDatesForCurrentUserGrouped = Object.keys(bookingDatesGrouped).filter((date) => { const bookingDates = bookingDatesGrouped[date] return bookingDates.length >= maxNumberOfBookingAtSameTime }) return disabledDatesForCurrentUserGrouped.map((date) => moment(date).toDate()) }