Building an appointment booking system in Rails
Asked Answered
B

4

6

I am looking to build an appointment booking app with the following characteristics: - Users can be service providers or buyers - Service providers set their availabilities (but can only set their availabilities at a maximum of 6 months ahead) - Buyers can then book appointments based on those availabilities - each appointment, based on the type of service, takes a different amount of time - Based on the appointment that a buyer selects, a different set of availabilities is shown depending on how long the service takes

What I've built is the following: - A TimeSlot model where I create a number of generic 30 minute time slots based on a start_time and end_time attribute. In order to make these time slots extend 6 months into the future, I have a background job running each day that creates all the new time slots necessary

class TimeSlot < ActiveRecord::Base
  has_many :user_time_slots
  # ... more methods below
end

- A UserTimeSlots model that basically represents a service provider's availability that they can set. So when they create a user_time_slot, they are essentially saying that they are available at that time.

class UserTimeSlot < ActiveRecord::Base 
    belongs_to :time_slot
    belongs_to :service_provider, :class_name => "User"
    belongs_to :appointment
end

- An Appointment model that has many user_time_slots. It has many because an appointment belongs to a service which takes a certain amount of time (a time_required attribute on services) and it might span a number consecutive user_time_slots.

class Appointment < ActiveRecord::Base
  has_many :user_time_slots
  belongs_to :buyer, :class_name => "User"
  belongs_to :service_provider, :class_name => "User"
  belongs_to :service
end

- A Service model that has many appointments and belongs to a service provider who creates that service.

class Service < ActiveRecord::Base
  has_many :appointments
  belongs_to :service_provider, :class_name => "User"
end

This domain model works; however, I am wondering if there is a better way to do this because of the following:

  • It seems a little clunky to me to be creating TimeSlot records every day on my backend using a background job - the TimeSlots really have the sole purpose of having a start and end time and then being associated to.

    • Once the user (buyer) selects a service they want, I am not sure how I would efficiently find the x number of user_time_slots that are consecutive and, therefore, available for booking the appointment (for example, if I have 30 minute time slot intervals and a user selects an appointment that will take 3 hours, I would have to find 6 consecutive time slots). For example, if a user clicks on an instance of a service I would have to 1) get the time required for that service (easy enough to do) and 2) I'd have to find ALL of the user's user_time_slots and collect their associated time_slots, then compare each time slot's start and end times to one another to find consecutive ones. This just seems like way too much iteration to me and seems like this will bog down my application!

Does anyone have a better way or solution to do this (particularly around the topic of finding consecutive time slots)?

Beaulahbeaulieu answered 20/5, 2015 at 18:35 Comment(2)
What kind of "credible and/or official sources" are you thinking of here?Rok
Really just looking for a well thought out solution to this problem here - no real official qualifications neededBeaulahbeaulieu
K
15

Look like a fun project. :)

I would personally not model the "existing time", i.e I would not have a background job create "empty" data.

I would try a model like this:

enter image description here

Where the User table?

I would not use a shared User model for the two different User types. My gut feeling is that they need to be different, if not now, most surly over time. Also it makes the model more clear, in my opinion. If there is a login or any auth, I would add a User table for that specific data and rather have Service Provider and Consumer have some relation to this.

Service Provider manages availability

The Service Provider would only need an empty calendar (or similar), listing only her already entered Service Availability, per Service.

Consumer books appointment

The Consumer would search/browse/navigate Service Availability, per Service

Depending on business logic, i.e. will a Consumer always schedule the whole defined time slot? Or could the Consumer book a preferred time slot. As long as the booked slot is within the given Service's available time slot?

Scheduling

So we only put Service Availability records in when the Service Provider manages her availability for a specific Service. Empty is simply considered not available.

We only put Appointment records in when a Consumer books an Service, based on Service Availability.

If its possible for the Consumer to book only a part of a Service Availability time slot, I would recommend to create two (or one) new Service Availability records for the time left over. Checking if time is available could be done by comparing Service Availability with Appointment, but this would not be optimal. Better would be to just query Service Availability for a specific Service and get a availability schedule, where no record would mean no availability.

There are a lots of ifs here, depending on your specifc needs and requested business logic. But I hope this helps with some inspiration and ideas.

Kisor answered 30/5, 2015 at 9:6 Comment(0)
S
6

I would have a single model for each availability period. Each period has a start_time and end_time. They can be easily validated to not be more than 6 months in advance, and you can check if an appointment will fit. Your time slot does not need to belong to an appointment as such; the appointment would simply have a time and a service provider, and said service provider's availability would change to reflect the booked appointment.

class AvailabilityPeriod < ActiveRecord::Base 
    belongs_to :service_provider, :class_name => "User"
    validates :end_time, :inclusion => { :in => Time.now..(Time.now + 6.months) }

end

For example, you could find all possible availabilities for a given service and provider like this:

duration = @service.duration
available_times = @provider.availability_periods.where (end_time - start_time > duration)

Booking an appointment is a bit more tricky. You need to split/shorten the availability period. For example, say a provider has the following availability:

May 30th 12:00 - 18:00

An appointment is booked for May 30th 14:00 - 16:00

The old availability needs to be removed and replaced by two new ones: May 30th 12:00 - 14:00 May 30th 16:00 - 18:00

But this is pretty easy to do in a model method.

Saline answered 27/5, 2015 at 18:11 Comment(0)
H
1

I like jpriebe's solution, though I would propose a slight change to the validation: use (Date.today + 6.months).end_of_day for the extreme end of the range.

The reason for my suggestion is to make the system slightly more user-friendly. If you used Time.now, and a user wants to create an AvailabilityPeriod ending at 4pm, but it is still somewhat early in the morning (say, around 10:15am), 4pm would be out of the range. To create the AvailabilityPeriod, they'd have to remember to come back later - and they might forget / are taking the lasagna out of the oven / insert distraction here.

Admittedly, there is really only one possible time in which this scenario could occur, and that is if a user is trying to create an AvailabilityPeriod exactly 6 months ahead (e.g., May 27 => Nov 27). Most of the time, I'm certain most users would not be setting up an AvailabilityPeriod that far in advance. Still, using .end_of_day would extend the acceptable time-range to the end of the appropriate day.

Honegger answered 27/5, 2015 at 21:18 Comment(0)
C
0

Rather than arbitrarily segmenting the timeslots, especially if few appointments are likely to be exactly one 30 minute interval, create Appointments with arbitrary start and stop times. Assuming Appointments will not span several days you can have a Day model that has_many Appointments. You will have to handle the overlap calculations programmatically, but you won't be beating your database to death to do so, you'll only need to join Day, Appointment and Providers and then run your calcuations to find your open slots. Appointments can be for 5 minutes or 6 hours. Doesn't matter.

Crippling answered 26/5, 2015 at 17:35 Comment(3)
That makes sense - however, how would a service provider set their availability? For example, if someone is available from 9am - 12pm then from 3pm - 6pm (and even more granular than that). Managing that seems like I need to have some other type of model associated with the day?Beaulahbeaulieu
That's what I would do. You can have other models to group appointments, days, availability, etc., like week, work-week, month, year, etc. This is all fairly rigid and arbitrary, which might be fine if you are looking at a simple, single-use app. An layer of abstraction gives you TimeBlock that has_many TimeBlocks through TimeUseAssociation that has_one TimeUseAssociationCategory. This is a circular reference, which makes most people's head hurt, so unless you are looking for generality I would go with the easier but more rigid models.Crippling
I would also like to point out that Postgresql provides range data types, including date ranges and date-time stamps (with and without timezone), that might make your handling of TimeBlocks easier.Crippling

© 2022 - 2024 — McMap. All rights reserved.