I need some people to battle a "best practice" for a PHP/MySQL appointment system for a hairdresser I'm currently working on. Hopefully, together we can clear some things up to avoid having to re-do the system afterwards. I've been looking at SO and Google for some tutorials, best practices etc. but I haven't found anything that fits the needs I have.
Basic Information
There are multiple hairdressers available each day, each hairdresser has his/her own agenda containing his/her appointments with customers. Connected to a hairdresser is a table containing the times he/she is available during the week.
Table: people
+----+------+-------+-----------+
| id | name | email | available |
+----+------+-------+-----------+
| 1 | John | [email protected] | Y |
| 2 | Sara | [email protected] | N |
+----+------+-------+-----------+
Table: times (there is a primary key for this table, I left it out of the schedule below)
+-----------+-------------+-----------+-----------+
| people_id | day_of_week | start | end |
+-----------+-------------+-----------+-----------+
| 1 | 1 | 08:00 | 12:00 |
| 1 | 1 | 12:30 | 17:00 |
| 1 | 3 | 09:00 | 12:00 |
| 1 | 4 | 10:30 | 16:00 |
| 1 | 5 | 09:00 | 17:00 |
| 1 | 6 | 09:00 | 12:00 |
| 2 | 1 | 09:00 | 12:00 |
| 2 | 2 | 09:00 | 12:00 |
| 2 | 3 | 09:00 | 12:00 |
+-----------+-------------+-----------+-----------+
As you can see there is a one to many relationship between people and times (obviously, since there are 7 days in a week). But besides that there is also an option to add a break between the hours of a day (see people_id = 1, day_of_week = 1: 08:00-12:00 and 12:30-17:00).
Furthermore there is a table called 'hairdress_types', this is a table containing the various types of appointments one can make (like coloring hair, cutting hair, washing, etc). This table contains the amount of time this appointment takes in minutes.
At last I have a table appointments
which is pretty simple:
id, people_id, types_id, sex, fullname, telephone, emailaddress, date_start, date_end
Date start and date end would be full DATETIME fields in MySQL making it easier to calculate using MySQL query functions.
So, the way I set things up, a front-end user would select a date in a field triggering an ajax/jquery function that finds all hairdressers available at the choosen date. The user then specificies a hairdresser (this is not mandatory: a user can also choose to select 'Any' hairdresser option) and the type of appointment he/she wants to make.
After submitting the first form, the following information can be used:
Using this information I would either select the available dates from the selected hairdresser OR I would loop al available hairdressers and their available dates.
This is where my minds gets a mental breakdown! Because what is the best way to check the available dates. The way I thought would be the best was:
SELECT * FROM appointments WHERE ((date_start >= $start AND date_start <= ($start+$time)) OR (date_end > $start AND date_end <= ($start+$time)) AND people_id = 1
)The biggers problem I'm facing is point 2: My mind is really going crazy on this query, is this the complete query I need to find appointments matching a specific timespan?
Thanks for reading & thinking along! :-)
John - Monday - 12:00 - 17:00. Appointments: - 12:00 - 12:30 - 14:30 - 15:30
A user wants to have an appointment which takes 2 hours, in the exampel above I would check:
Thus.. it might be a better option to use "timeblocks" of 10/15 minutes. Making the checks:
This would find the available spot between 12:30 and 14:30.
I've been working out some stuff on paper (a table with appointments and possible empty spots to use). And I came up with the following. An appointment can not be made in case:
Querying the above to the appointment table together with people_id
would result in either no rows (= free spot) or one/multiple row(s) in which case the spot is taken.
I guess the best way to find open spots is to query the database for blocks of X minutes with a start interval of 10 minutes. The bad side of this solution is that I would neet 6 queries for every hour, which would be about 48 queries for every hairdresser... Any ideas on how to reduce that amount of queries?
In the end I went for a system that generated timestamps for the start and end dates in the database. While checking I added one second to the start and subtracted one second from the end to avoid any overlapping time for appointments.
What did I end up doing
Obviously I'm not sure this is the best practice, but it did work for me. The user starts by selecting their sex and a preference for the day. This sends an AJAX request to get the available personel and various kinds of appointment types (f.e. coloring hair, cutting hair, etc).
When all settings have been choosen (sex, date, personel and type) I start by some simple validations: checking the date, checking if the date("N") is not 7 (sunday). If everything is okay, the more important stuff is started:
1) The appointment type is fetched from the database including the total amount of time this type takes (30 minutes, 45 minutes, etc) 2) The available personal is fetched (a complete list of people on that day or just a single person if one is chosen) including their available times
The personel (or one person) is then looped, starting with their own starting time. At this point I have a set of data containing:
$duration (of the appointment type)
$startTime (starting time of the person selected in the loop)
$endTime (= $startTime + $duration)
$personStart (= starting time of the person)
$personEnd (= end time of the person)
Let's take this demo data:
$duration = 30 min
$startTime = 9.00h
$endTime = 9.30h
$personStart = 9.00h
$personEnd = 12.00h
What I'm doing here is:
while( $endTime < $personEnd )
{
// Check the spot for availability
$startTime = $endTime;
$endTime = $startTime + $duration;
}
Obviously, it's al simplified in this case. Because when I check for availability, and the spot is not free. I set the $startTime to be equal to the latest appointment found and start from there in the loop.
Example:
I check for a free spot at 9.00 but the spot is not free because there's an appointment from 9.00 till 10.00, then 10.00 is returned and $startTime is set to 10.00h instead of 9.30h. This is done to keep the number of queries to a minimum since there can be quiet a lot.
Check availability function
// Check Availability
public static function checkAvailability($start, $end, $ape_id)
{
// add one second to the start to avoid results showing up on the full hour
$start += 1;
// remove one second from the end to avoid results showing up on the full hour
$end -= 1;
// $start and $end are timestamps
$getAppointments = PRegistry::getObject('db')->query("SELECT * FROM appointments WHERE
((
app_start BETWEEN '".date("Y-m-d H:i:s", $start)."' AND '".date("Y-m-d H:i:s", $end)."'
OR
app_end BETWEEN '".date("Y-m-d H:i:s", $start)."' AND '".date("Y-m-d H:i:s", $end)."'
)
OR
(
app_start < '".date("Y-m-d H:i:s", $start)."' AND app_end > '".date("Y-m-d H:i:s", $end)."'
))
AND
ape_id = ".PRegistry::getObject('db')->escape($ape_id));
if(PRegistry::getObject('db')->num_rows($getAppointments) == 0) {
return true;
} else {
$end = 0;
foreach(PRegistry::getObject('db')->fetch_array(MYSQLI_ASSOC, $getAppointments, false) as $app) {
if($app['app_end'] > $end) {
$end = $app['app_end'];
}
}
return $end;
}
}
Since I'm storing appointments as "From:10.00 Till:11.00" I have to make sure to check spots from 11:00:01 till 11:59:59, because otherwise the appointment at 11:00 will show in the results.
At the end of the function, in case an appointment is found, I loop the results and return the latest end. This is the next start in the loop I mentioned above.
Hopefully this can be of any help to anyone. Just as info: ape_id
is the ID of the "Appointment Person" it is linked with.