Building an appointment booking system in Rails - ruby-on-rails

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)?

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:
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.

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.

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.

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.

Related

How to run a callback function on two attributes that depend on each other?

In my Rails app I have an Invoice model with the attributes date and due_date.
For reasons of simplicity I don't want the user to manually enter the due_date but rather simply enter the number of days that should be added to the date.
This is why I set up a virtual attribute days_allowed.
class Invoice < ActiveRecord::Base
belongs_to :user
before_save :save_date
attr_accessor :days_allowed
def days_allowed # attribute reader (I need that too!)
(due_date - date).to_i
end
def save_date
self.due_date = date + days_allowed.days
end
end
However, when a user picks a date in the form and enters a number of days, e.g. 10, I get wrong results because the save_date callback refers to the days_allowed method rather than the attribute of the same name.
The key problem seems to be that I am using the callback on two different attributes that depend on each other (date and days_allowed).
Can anybody tell me how to solve this puzzle?
Thanks for any help.
How about this approach (no before_save is necessary):
class Invoice < ActiveRecord::Base
belongs_to :user
def days_allowed
(due_date - date).to_i
end
def days_allowed=(days)
self.due_date = date + days
end
end
EDIT Not supposed to work with mass assignment when both date and days_allowed are present unless date always goes first.
I think you need to store all three fields, just not provide any form attributes for editing the calculated one.
While your use-case at it is might seem like this is a violation of 'don't repeat yourself', I think this is an instance of coincidental duplication. Let me explain:
Consider your next problem - you are going to start having due dates on weekends, holidays, etc. For exactly that reason I wrote the business_time gem:
https://github.com/bokmann/business_time
so in your solution where you add 10 days, you can now add 10 business days. The exact due date can now fluctuate across weekends, 3-day weekends created by holidays, etc. And if you have to deal with snow days, like most of the East Coast of the U.S. has had to lately, you can just add tham as a holiday and recalculate. The number of business days is still 10, but the due date has changed. The number of business days was a critical piece of data to save, not a piece of data to calculate. The due data needs to be saved, because its a piece of information calculated in a context that you'll want to save for querying rather than try to recalculate every time.

Recurring Events in Calendar - Rails

I am searching for the best way to model recurring events. I am using fullcalendar to display events. But I guess recurring events are best handled on the rails backend.
I already looked at other questions and existing example code but I didn't find anything which fits.
It should behave similar like google calendar. So it should be possible to delete/modify single events of the recurring event series. But saving all events of the event series in the database seems inefficient. Also it should be possible to create single events without any recurrence.
What would be a good model architecture?
My event model right now looks like that (without additional attributes):
# Table name: events
#
# id :integer not null, primary key
# employee_id :integer
# created_at :datetime
# updated_at :datetime
# starts_at :datetime
# ends_at :datetime
#
class Event < ActiveRecord::Base
attr_accessible :starts_at, :ends_at
end
Here is how I would model this. I haven't used Google Calendar much, so I'm basing the functionality off of iCal's recurring events.
All models should have the usual id, created_at, updated_at properties. Listed are the custom properties. If the property is another model, you will implement it an association such as has_one or belongs_to.
RecurrencePeriod
Event base_event # has_one :base_event, :class_name'Event'
Time end_date # may be nil, if it recurs forever
WeeklyRecurrence recurrence # has_one :recurrence, :as=>:recurrence
Array[OccurrenceOverride] overrides # has_many :overrides, :class_name=>'OccurrenceOverride'
The RecurrencePeriod starts on the date that its base_event starts. Also, I assume that an Event's employee_id refers to the employee that created that event. A RecurrencePeriod will also belong to the employee that created the base_event.
The model depends on how flexibly you want to be able to specify recurrences. Are you going to support "Tuesday and Thursday every two weeks from 10 AM to 11 AM and from 2 PM to 3 PM" or just "repeats weekly"? Here's a model that supports just "repeats weekly", "repeats every two weeks", etc.; you can expand it if you need to.
WeeklyRecurrence
Integer weeks_between_recurrences
RecurrencePeriod recurrence_period # belongs_to :recurrence, :polymorphic=>true
I use polymorphic associations here, because I think they might be useful if you want more than one type of recurrence, such both WeeklyRecurrence and DailyRecurrence. But I'm not sure that they're the correct way to model that, so if they turn out not to be, just use has_one :weekly_recurrence and belongs_to :recurrence_period instead.
The Ice cube library seems like it might be useful for calculating recurrences. If WeeklyRecurrence above isn't powerful enough, you might just want to store an Ice cube Schedule object in a model, replacing WeeklyRecurrence. To store a Schedule object in a model, save it as an attribute "schedule", put serialize :schedule in the model definition, and generate a text column "schedule" in the database.
OccurrenceOverride handles the case of a single instance of a recurring event being edited.
OccurrenceOverride
RecurrencePeriod recurrence_period_to_override # belongs_to :recurrence_period_to_override, :class_name=>'RecurrencePeriod'
Time original_start_time # uniquely identifies which recurrence within that RecurrencePeriod to replace
Event replacement_event # has_one :replacement_event, :class_name=>'Event'; may be nil, if that recurrence was deleted instead of edited
Instead of storing each occurrence of an event individually, generate them temporarily when you need to show them in the view. In RecurrencePeriod, create a method generate_events_in_range(start_date, end_date) that generates Events, not to save in the database, but just to pass to the view so it can show them.
When a user edits a recurrence, they should have the option to modify all occurrences, all future occurrences, or just that event. If they modify all occurrences, modify the RecurrencePeriod's base_event. If they modify all future occurrences, use a method you should implement on RecurrencePeriod that splits itself into two RecurrencePeriods on either side of a certain date, and then save the changes to just the second period. If they modify only that event, create an OccurrenceOverride for the time that they are overriding, and save the changes to the override's replacement_event.
When a user says a certain event should now recur every two weeks for the foreseeable future, you should create a new RecurrencePeriod with that event as base_event and a nil end_date. Its recurrence should be a new WeeklyRecurrence with weeks_between_recurrence=2, and it should have no OccurrenceOverrides.
In my case I did something like this :
# Holds most of my event's data; name, description, price ...
class Event < ActiveRecord::Base
has_many :schedules
has_many :occurrences
attr_accessible :started_at, :expired_at # expired_at is optional
end
# Holds my schedule object
class Schedule < ActiveRecord::Base
belongs_to :event
attr_accessible :ice_cube_rule # which returns my deserialized ice_cube object
end
# Holds generated or manually created event occurrences
class Occurrence < ActiveRecord::Base
belongs_to :event
attr_accessible :started_at, :expired_at
attr_accessible :generated # helps me tell which occurrences are out of an ice_cube generated serie
attr_accessible :canceled_at
end
From there, I used ice_cube to manage the occurrences calculation and stored the results in the occurrences table. I first tried to work without the Occurrence model, but no matter how advanced the rule engine, you'll always have exceptions, so storing the occurrences in their own model gives you flexibility.
Having an Occurrence model makes it a lot easier to display the events on a calendar or with date search filters as you just need to query for occurrences and then display the related event's data instead of gathering all the events in a given date range and then having to filter out the events where the schedule(s) don't match.
Also you can flag an event occurrence as canceled or modify it (setting the generated attribute at false so it does not get cleaned up when editing an ice_cube schedule... or whatever your business need is)
Of course if you have events that repeat indefinitely, you'll want to limit how far in the future you want those occurrences to be generated and use automated rake tasks to clean up the old ones and generate occurrences for the next year or so.
So far this pattern works pretty well for me.
Also, take a look at the recurring_select gem which is a pretty neat ice_cube form input.
Just an opinion off the top of my head, perhaps comenters will point out a problem I'm not thinking of at the moment:
I would make a RecurringEvent model (or whatever you want to call it) that has_many :events.
Let's say each event is created by an employee (based on your notes), then RecurringEvent would also belong_to :employee. You could then build a has_many :through relationship where an employee has many events and has many recurring events.
The RecurringEvent model could have a start date and a pattern, and it could initially use this pattern to create the individual occuring events. Then on any event that is part of the recurring series you could modify or delete that individual occurance, but you could also 'regenerate the series', deleting all events in the series (or all future events in the series) and rebuilding them based on a new pattern, so for instance move the meeting from "every tuesday" to "every thursday".
One other kind of nice thing about this is you could create an at-a-glance list of recurring events, which might give you some nice insight into people's major obligations.
Like I said, off the top of my head that's how I would approach it, but this is just an idea and I haven't built anything like that so I don't know if there are any big gotchas in the approach I'm suggesting.
Good luck, please post what you end up doing!
I m quite new to Rails, your solution sounds interesting. To create the schedule and associated occurences, do you use conditionnal callbacks in Event model?
In my case, users would be able to create events, weekly recurring or not. So I was thinking about a recurring boolean field in event model. So I guess you would have a first callback to create the schedule:
before_save :create_weekly_schedule, if: :recurring
and basically a second one to create the occurences:
after_save :create_occurences_if_recurring
def create_occurences_if_recurring
schedules.each do |sched|
occurences.create(start_date: sched.start_time, end_date: sched.end_time)
end
end
Does this sound logical with your solution?
Thx

Where in Rails to fill in gaps in time series?

My basic set of models looks roughly like this (simplified):
[ PayPeriod ]
id
start_date
end_date
notes
[ Transaction ]
id
amount
fkPayPeriod
When a user logs in, I want to present them with a list of the last 4 pay periods. The user can select a pay period to enter notes and transactions.
I'm inclined to just do something like this in the controller:
def index
#pay_periods = PayPeriod.get_or_create_last(4)
end
The PayPeriod model itself would implement the behavior, and records would be created "on demand" as users used the app.
An alternative approach might be to have some other (background) process that combs through accounts on a daily basis to proactively create new PayPeriods. Although neither solution strikes me as particularly challenging, I'd like to get some feedback on where others feel this functionality belongs conceptually.
I'd say it depends on what a PayPeriod is, conceptually. Is it a universal description of time periods on a common calendar (i.e., the year 2011 has 26 pay periods of two weeks each)? If so, then the creation of all the PayPeriods belongs in your db/seeds.rb file.
If a PayPeriod belongs to an account, I'd say that the creation of the last four PayPeriods belongs in an after_create hook on the account in question. Going forward from there, I think the best practice would be to create new PayPeriods as your user needs them; that is, the first time I go to add notes or a transaction to a PayPeriod is probably when it should get created.
If for any reason you have a system policy in place that allows users to edit a specific window of PayPeriods (e.g., you can edit this and the next calendar year), then you might set up a Rake task and hook it up to a Cron job that runs it however often it needs to be run (e.g., if you're limiting your window by calendar year, then you could set up a Cron job to run every January 1 at 12:01am that creates the year's PayPeriods).

Rails 3: Automatically create entries in table using preset values

Currently, I'm building a scheduling system for my workplace. It's a highly-trafficked university tour guide office, and given a schedule of the day's events and the shifts of all available tour guides, the system will assign tours to available tour guides.
But, when asking a user to build the event schedule (or list of tours) for the day, I'd prefer to have a schedule auto-generated that they can then manipulate. There are only two different types of schedules that alternate depending on the day of the week, and this head start will save a lot of time.
My question: Where do I put this 'seed' data? Should I create a YAML and then have a method read it to populate a given day with appropriate defaults?
For reference, my models are currently structured like so:
guide has_many :tours, :shifts
shifts belongs_to :guide
tour belongs_to :guide
I've considered a 'day' model but as every time is a datetime, I felt like this was too redundant to persist to the database.
Thanks! All suggestions welcome. I'm just trying to get a good grip on the proper "Rails Way" to handle this.
You should use a factory to initialize your objects. Have a look at https://github.com/thoughtbot/factory_girl

Appointment scheduling in Rails?

I have a simple application with a customer and appointment models, system admin can create customer and create appointments for that particular customer, is there a Rails plugin that handles scheduling to ensure no two appointments overlap each other? i.e no two appointments at the same time.
A plus would be if I can set up more than one schedule, i.e shop has 2 instructors for lessons, when selecting appointment we can select which instructor etc.
What's the best way to do this?
Thanks
def is conflicting
if(appointemnt1_start < appointment2_end && appointment2.start < appointment1.end)
return true
end
end
Not that I know of. It's fairly simple in theory, though. Subtract Time B from Time A, and if they're within X minutes of each other (however long an appointment takes), there's a conflict.

Resources