///<summary>Adds valid time slots to listAvailableTimeSlots if the time slot found does NOT already exist within the list. ///This is a helper method to better break up the complexity of GetAvailableWebSchedTimeSlots() so that it is easier to follow. ///Make sure that timePattern is always passed in utilizing 5 minute increments (no conversion will be applied to the pattern passed in). ///Optionally set defNumApptType if looking for time slots for New Pat Appt which will apply the DefNum to all time slots found.</summary> private static void AddTimeSlotsFromSchedule(List <TimeSlot> listAvailableTimeSlots, Schedule schedule, long operatoryNum , TimeSpan timeSchedStart, TimeSpan timeSchedStop, List <Schedule> listBlockouts , Dictionary <DateTime, List <ApptSearchProviderSchedule> > dictProvSchedules, List <Appointment> listApptsForOps, string timePattern , long defNumApptType = 0) { //No need to check RemotingRole; no call to db and this is a private method. //Figure out how large of a time slot we need to find in order to consider this time slot "available". int apptLengthMins = timePattern.Length * 5; int timeIncrement = PrefC.GetInt(PrefName.AppointmentTimeIncrement); DateTime dateSched = schedule.SchedDate; //Start going through this operatories schedule according to the time increment, looking for a gap that can handle apptLengthMins. TimeSpan timeSlotStart = new TimeSpan(timeSchedStart.Ticks); //Start looking for collisions AFTER the start time. //Stop as soon as the slots stop time meets or passes the sched stop time. //Iterate through the schedule via the time increment preference. for (TimeSpan timeSlotStop = timeSchedStart.Add(new TimeSpan(0, timeIncrement, 0)) ; timeSlotStop <= timeSchedStop ; timeSlotStop = timeSlotStop.Add(new TimeSpan(0, timeIncrement, 0))) { //Check to see if we've found an opening. TimeSpan timeSpanCur = timeSlotStop - timeSlotStart; //Check to see if there is an appointment or a blockout that collides with this blockout. bool isOverlapping = false; //First we'll look at blockouts because it should be quicker than looking at the appointments foreach (Schedule blockout in listBlockouts) { if (dateSched.Date != blockout.SchedDate) { continue; //Blockout is not on the same day that we are looking at. } if (!blockout.Ops.Exists(x => x == operatoryNum)) { continue; //Blockout is not present in this operatory. } //Same operatory and day, check if the times overlap. //Create new TimeSpans in order to remove the date portion from the blockouts. TimeSpan timeBlockoutStart = new TimeSpan(blockout.StartTime.Hours, blockout.StartTime.Minutes, 0); TimeSpan timeBlockoutStop = new TimeSpan(blockout.StopTime.Hours, blockout.StopTime.Minutes, 0); if (IsTimeOverlapping(timeSlotStart, timeSlotStop, timeBlockoutStart, timeBlockoutStop)) { isOverlapping = true; break; } } if (isOverlapping) //This check is here so that we don't waste time looping through appointments if we don't need to. //There was a collision, set the time slot start time to the stop time and continue from there. { timeSlotStart = timeSlotStop; continue; } //Next we'll look for overlapping appointments foreach (Appointment appointment in listApptsForOps) { if (appointment.Op != operatoryNum) { continue; } if (dateSched.Date != appointment.AptDateTime.Date) { continue; //Appt is not on the same day that we are looking at. } //Same operatory and day, check if the times overlap. TimeSpan timeApptStart = appointment.AptDateTime.TimeOfDay; TimeSpan timeApptStop = appointment.AptDateTime.AddMinutes(appointment.Pattern.Length * 5).TimeOfDay; if (IsTimeOverlapping(timeSlotStart, timeSlotStop, timeApptStart, timeApptStop)) { isOverlapping = true; break; } } if (isOverlapping) { //There was a collision, set the time slot start time to the stop time and continue from there. timeSlotStart = timeSlotStop; continue; } if (timeSpanCur.TotalMinutes >= apptLengthMins) { //We just found an opening. Make sure we don't already have this time slot available. DateTime dateTimeSlotStart = new DateTime(dateSched.Year, dateSched.Month, dateSched.Day, timeSlotStart.Hours, timeSlotStart.Minutes, 0); DateTime dateTimeSlotStop = new DateTime(dateSched.Year, dateSched.Month, dateSched.Day, timeSlotStop.Hours, timeSlotStop.Minutes, 0); TimeSlot timeSlot = new TimeSlot(dateTimeSlotStart, dateTimeSlotStop, operatoryNum, schedule.ProvNum, defNumApptType); if (!listAvailableTimeSlots.Any(x => (x.DateTimeStart == dateTimeSlotStart && x.DateTimeStop == dateTimeSlotStop && x.ProvNum == schedule.ProvNum))) //We will return multiple time slots for the same time for different providers. { //This time slot is not already in our list of available time slots, check for double booking. if (dictProvSchedules.ContainsKey(dateSched.Date)) { long recallProvNum = schedule.ProvNum; if (IsApptPatternDoubleBooked(dictProvSchedules[dateSched.Date], recallProvNum, timePattern, dateTimeSlotStart)) { //There is a double booking conflict. Do not add this time slot as a possibility. //However, at this point we know that there are no appointment conflicts for the current time slot, only a double booking conflict. //The appointment needs to scoot within the operatory to hopefully find the first available opening (unit test 86). timeSlotStart = timeSlotStart.Add(new TimeSpan(0, timeIncrement, 0)); continue; } } //There are no collisions with this provider's schedule, add it to our list of available time slots. listAvailableTimeSlots.Add(timeSlot); } else { //We have found a time slot in another operatory that matches the necessary criteria. //Check to see if this operatory should be considered before the previously found operatory. TimeSlot timeSlotCur = listAvailableTimeSlots.First(x => (x.DateTimeStart == dateTimeSlotStart && x.DateTimeStop == dateTimeSlotStop)); Operatory operatoryIn = Operatories.GetOperatory(operatoryNum); Operatory operatoryCur = Operatories.GetOperatory(timeSlotCur.OperatoryNum); if (operatoryIn.ItemOrder < operatoryCur.ItemOrder) { timeSlotCur.OperatoryNum = operatoryIn.OperatoryNum; } } //Continue looking for more open slots starting at the end of this time slot. //E.g. we just found 9:30 AM to 10:00 AM. We need to continue from 10:00 AM. timeSlotStart = timeSlotStop; continue; } } }
///<summary>Adds valid time slots to listAvailableTimeSlots if the time slot found does NOT already exist within the list. ///This is a helper method to better break up the complexity of GetAvailableWebSchedTimeSlots() so that it is easier to follow. ///Make sure that timePattern is always passed in utilizing 5 minute increments (no conversion will be applied to the pattern passed in). ///Optionally set defNumApptType if looking for time slots for New Pat Appt which will apply the DefNum to all time slots found.</summary> private static void AddTimeSlotsFromSchedule(List <TimeSlot> listAvailableTimeSlots, Schedule schedule, long operatoryNum , TimeSpan timeSchedStart, TimeSpan timeSchedStop, List <Schedule> listBlockouts , Dictionary <DateTime, List <ApptSearchProviderSchedule> > dictProvSchedules, List <Appointment> listApptsForOps, string timePattern , long defNumApptType = 0, bool isDoubleBookingAllowed = true) { //No need to check RemotingRole; no call to db and this is a private method. //Figure out how large of a time slot we need to find in order to consider this time slot "available". int apptLengthMins = timePattern.Length * 5; int timeIncrement = PrefC.GetInt(PrefName.AppointmentTimeIncrement); DateTime dateSched = schedule.SchedDate; //Filter out all blockouts that are not pertinent to this dateSched and operatoryNum combo. List <Schedule> listBlockoutsForDateAndOp = listBlockouts.FindAll(x => x.SchedDate.Date == dateSched.Date && x.Ops.Contains(operatoryNum)); //Filter out all appointments that are not pertinent to this dateSched and operatoryNum combo. List <Appointment> listApptsForDateAndOp = listApptsForOps.FindAll(x => x.AptDateTime.Date == dateSched.Date && x.Op == operatoryNum); //Start going through this operatory's schedule according to the time increment, looking for a gap that can handle apptLengthMins. TimeSpan timeSlotStart = new TimeSpan(timeSchedStart.Ticks); //Make a list of all perfect world appointment starting times that we will use within our time slot finding loop to make our slot finding //more predictable and user friendly. This is mainly for the scenario where offices manually schedule strange appointments throughout the day. //E.g. Searching for hour long time slots, a 15 min appt was manually scheduled for 09:55 - 10:10 which throws off the nice "on the hour" slots. //We want to have logic that will prefer to return time slots on the hour. E.g. 8 - 9, 10:10 - 11: 10, 11 - 12 (note the overlap), 12 - 13... List <TimeSpan> listPerfectSlotStarts = new List <TimeSpan>(); //The first perfect time slot start will always be when our schedule starts. for (TimeSpan timeSlotPerfect = new TimeSpan(timeSchedStart.Ticks) ; timeSlotPerfect <= new TimeSpan(timeSchedStop.Ticks) ; timeSlotPerfect = timeSlotPerfect.Add(new TimeSpan(0, apptLengthMins, 0))) { listPerfectSlotStarts.Add(timeSlotPerfect); } //Start looking for collisions AFTER the start time. //Stop as soon as the slots stop time meets or passes the sched stop time. //Iterate through the schedule via the time increment preference. for (TimeSpan timeSlotStop = timeSchedStart.Add(new TimeSpan(0, timeIncrement, 0)) ; timeSlotStop <= timeSchedStop ; timeSlotStop = timeSlotStop.Add(new TimeSpan(0, timeIncrement, 0))) { //Check to see if we've found an opening. TimeSpan timeSpanCur = timeSlotStop - timeSlotStart; //Check to see if there is an appointment or a blockout that collides with this blockout. bool isOverlapping = false; #region Blockout Collisions TimeSpan timeBlockoutStart = new TimeSpan(); TimeSpan timeBlockoutStop = new TimeSpan(); //First we'll look at blockouts because it should be quicker than looking at the appointments foreach (Schedule blockout in listBlockoutsForDateAndOp) { //Create new TimeSpans in order to remove the date portion from the blockouts. timeBlockoutStart = new TimeSpan(blockout.StartTime.Hours, blockout.StartTime.Minutes, 0); timeBlockoutStop = new TimeSpan(blockout.StopTime.Hours, blockout.StopTime.Minutes, 0); if (IsTimeOverlapping(timeSlotStart, timeSlotStop, timeBlockoutStart, timeBlockoutStop)) { isOverlapping = true; break; } } if (isOverlapping) //This check is here so that we don't waste time looping through appointments if we don't need to. //There was a collision with a blockout. Set the time slot start time to the stop time of the blockout and continue from there. { timeSlotStart = timeBlockoutStop; continue; } #endregion #region Appointment Collisions TimeSpan timeApptStart = new TimeSpan(); TimeSpan timeApptStop = new TimeSpan(); //Next we'll look for overlapping appointments foreach (Appointment appointment in listApptsForDateAndOp) { timeApptStart = appointment.AptDateTime.TimeOfDay; timeApptStop = appointment.AptDateTime.AddMinutes(appointment.Pattern.Length * 5).TimeOfDay; if (IsTimeOverlapping(timeSlotStart, timeSlotStop, timeApptStart, timeApptStop)) { isOverlapping = true; break; } } if (isOverlapping) { //There was a collision with an appointment. Set the time slot start time to the stop time of the appointment and continue from there. timeSlotStart = timeApptStop; continue; } #endregion #region Opening Found if (timeSpanCur.TotalMinutes >= apptLengthMins) { //We just found an opening. Make sure we don't already have this time slot available. DateTime dateTimeSlotStart = new DateTime(dateSched.Year, dateSched.Month, dateSched.Day, timeSlotStart.Hours, timeSlotStart.Minutes, 0); DateTime dateTimeSlotStop = new DateTime(dateSched.Year, dateSched.Month, dateSched.Day, timeSlotStop.Hours, timeSlotStop.Minutes, 0); TimeSlot timeSlot = new TimeSlot(dateTimeSlotStart, dateTimeSlotStop, operatoryNum, schedule.ProvNum, defNumApptType); if (!listAvailableTimeSlots.Any(x => (x.DateTimeStart == dateTimeSlotStart && x.DateTimeStop == dateTimeSlotStop && x.ProvNum == schedule.ProvNum))) //We will return multiple time slots for the same time for different providers. { //This time slot is not already in our list of available time slots, check for double booking. if (dictProvSchedules.ContainsKey(dateSched.Date)) { long recallProvNum = schedule.ProvNum; if (IsApptTimeSlotDoubleBooked(dictProvSchedules[dateSched.Date], listApptsForDateAndOp, recallProvNum, timePattern, dateTimeSlotStart , defNumApptType, isDoubleBookingAllowed)) { //There is a double booking conflict. Do not add this time slot as a possibility. //However, at this point we know that there are no appointment conflicts for the current time slot, only a double booking conflict. //The appointment needs to scoot within the operatory to hopefully find the first available opening (unit test 86). timeSlotStart = timeSlotStart.Add(new TimeSpan(0, timeIncrement, 0)); continue; } } //There are no collisions with this provider's schedule, add it to our list of available time slots. listAvailableTimeSlots.Add(timeSlot); //Check to see if the time slot that was just added started on a "perfect starting time". //If it didn't, we need to backtrack to the most recent "perfect starting time" and continue from there. if (!listPerfectSlotStarts.Contains(timeSlotStart)) { //Find the most recent "perfect starting time" that corresponds to the timeSlotStop that was just found. //We then need to set both timeSlotStart AND timeSlotStop to the closest "perfect starting time" and continue from there. //E.g. If apptLengthMins is 60 minutes, odds are we are looking for appointments that start on the hour. //So if we just found an opening that started at 10:10 then we need to backtrack from the stopping time of 11:10 and find the closest //"perfect starting time", 11:00 in this case, and continue searching from there. timeSlotStart = listPerfectSlotStarts.Last(x => x.Subtract(timeSlotStop).TotalMinutes < 0); timeSlotStop = timeSlotStart; } } else { //We have found a time slot in another operatory that matches the necessary criteria. //Check to see if this operatory should be considered before the previously found operatory. TimeSlot timeSlotCur = listAvailableTimeSlots.First(x => (x.DateTimeStart == dateTimeSlotStart && x.DateTimeStop == dateTimeSlotStop)); Operatory operatoryIn = Operatories.GetOperatory(operatoryNum); Operatory operatoryCur = Operatories.GetOperatory(timeSlotCur.OperatoryNum); if (operatoryIn.ItemOrder < operatoryCur.ItemOrder) { timeSlotCur.OperatoryNum = operatoryIn.OperatoryNum; } } //Continue looking for more open slots starting at the end of this time slot. //E.g. we just found 9:30 AM to 10:00 AM. We need to continue from 10:00 AM. timeSlotStart = timeSlotStop; continue; } #endregion } }