Example #1
0
        ///<summary>Gets open time slots based on the parameters passed in.
        ///Open time slots are found by looping through the passed in operatories and finding openings that can hold the entire appointment.
        ///Make sure that timePattern is always passed in utilizing 5 minute increments (no conversion will be applied to the pattern passed in).
        ///Providers passed in will be the only providers considered when looking for available time slots.
        ///Passing in a null clinic will only consider operatories with clinics set to 0 (unassigned).
        ///The timeslots on and between the Start and End dates passed in will be considered and potentially returned as available.
        ///Optionally set defNumApptType if looking for time slots for New Pat Appt which will apply the DefNum to all time slots found.
        ///Throws exceptions.</summary>
        public static List <TimeSlot> GetTimeSlotsForRange(DateTime dateStart, DateTime dateEnd, string timePattern, List <long> listProvNums
                                                           , List <Operatory> listOperatories, List <Schedule> listSchedules, Clinic clinic, long defNumApptType = 0, Logger.IWriteLine log = null)
        {
            //No need to check RemotingRole; no call to db.
            //Order the operatories passed in by their ItemOrder just in case they were passed in all jumbled up.
            List <long> listOpNums = listOperatories.OrderBy(x => x.ItemOrder).Select(x => x.OperatoryNum).Distinct().ToList();

            //Remove all schedules that fall outside of the date range passed in.  Only consider the date right now, the time portion is handled later.
            listSchedules.RemoveAll(x => !x.SchedDate.Date.Between(dateStart.Date, dateEnd.Date));
            List <Schedule> listProviderSchedules = listSchedules.FindAll(x => x.BlockoutType == 0);
            List <Schedule> listBlockoutSchedules = listSchedules.FindAll(x => x.BlockoutType > 0);
            //Get every single appointment for all operatories within our start and end dates for double booking and overlapping consideration.
            List <Appointment> listApptsForOps = Appointments.GetAppointmentsForOpsByPeriod(Operatories.GetDeepCopy(true).Select(x => x.OperatoryNum).ToList()
                                                                                            , dateStart, dateEnd, log, listSchedules.GroupBy(x => x.ProvNum).Select(x => x.Key).ToList());

            log?.WriteLine("listProviderSchedules:\r\n\t" + string.Join(",\r\n\t",
                                                                        listProviderSchedules.Select(x => x.ScheduleNum + " - " + x.SchedDate.ToShortDateString() + " " + x.StartTime)), LogLevel.Verbose);
            log?.WriteLine("listBlockoutSchedules:\r\n\t" + string.Join(",\r\n\t",
                                                                        listBlockoutSchedules.Select(x => x.ScheduleNum + " - " + x.SchedDate.ToShortDateString() + " " + x.StartTime)), LogLevel.Verbose);
            log?.WriteLine("listApptsForOps:\r\n\t"
                           + string.Join(",\r\n\t", listApptsForOps.Select(x => x.AptNum + " - " + x.AptDateTime + " OpNum: " + x.Op)), LogLevel.Verbose);
            //We need to be conscious of double booking possibilities.  Go get provider schedule information for the date range passed in.
            Dictionary <DateTime, List <ApptSearchProviderSchedule> > dictProvSchedules = Appointments.GetApptSearchProviderScheduleForProvidersAndDate(
                listProvNums, dateStart, dateEnd, listProviderSchedules, listApptsForOps);
            //Split up the operatory specific provider schedules from the dynamic ones because each will have different operatory logic.
            List <Schedule>   listProviderSchedulesWithOp = listProviderSchedules.FindAll(x => x.Ops.Intersect(listOpNums).ToList().Count > 0);
            List <ScheduleOp> listScheduleOps             = ScheduleOps.GetForSchedList(listProviderSchedules);
            //Now we need to get the dynamic schedules (not assigned to a specific operatory).
            List <Schedule> listProviderDynamicSchedules = listProviderSchedules.FindAll(x => !listScheduleOps.Exists(y => y.ScheduleNum == x.ScheduleNum));
            //Now that we have found all possible valid schedules, find all the unique time slots from them.
            List <Schedule> listProviderSchedulesAll = new List <Schedule>(listProviderSchedulesWithOp);

            listProviderSchedulesAll.AddRange(listProviderDynamicSchedules);
            listProviderSchedulesAll = listProviderSchedulesAll.OrderBy(x => x.SchedDate).ToList();
            List <TimeSlot> listAvailableTimeSlots = new List <TimeSlot>();
            List <DateTime> listUniqueDays         = new List <DateTime>();
            int             timeIncrement          = PrefC.GetInt(PrefName.AppointmentTimeIncrement);

            //Loop through all schedules five minutes at a time to find time slots large enough that have no appointments and no blockouts within them.
            foreach (Schedule schedule in listProviderSchedulesAll)
            {
                DateTime dateSched = schedule.SchedDate;
                //Straight up ignore schedules in the past.  This should not be possible but this is just in case.
                if (dateSched.Date < DateTime.Today)
                {
                    continue;
                }
                if (!listUniqueDays.Contains(dateSched))
                {
                    listUniqueDays.Add(dateSched);
                }
                TimeSpan timeSchedStart = schedule.StartTime;
                TimeSpan timeSchedStop  = schedule.StopTime;
                //Now, make sure that the start time is set to a starting time that makes sense with the appointment time increment preference.
                int minsOver = (timeSchedStart.Minutes) % timeIncrement;
                if (minsOver > 0)
                {
                    int minsToAdd = timeIncrement - minsOver;
                    timeSchedStart = timeSchedStart.Add(new TimeSpan(0, minsToAdd, 0));
                }
                //Double check that we haven't pushed the start time past the stop time.
                if (timeSchedStart >= timeSchedStop)
                {
                    continue;
                }
                //Figure out all possible operatories for this particular schedule.
                List <Operatory> listOpsForSchedule = new List <Operatory>();
                if (schedule.Ops.Count > 0)
                {
                    listOpsForSchedule = listOperatories.FindAll(x => schedule.Ops.Exists(y => y == x.OperatoryNum));
                }
                else                  //Dynamic schedule.  Figure out what operatories this provider is part of that are associated to the corresponding eService.
                                      //Get all of the valid operatories that this provider is associated with.
                {
                    listOpsForSchedule = listOperatories.FindAll(x => x.ProvDentist == schedule.ProvNum || x.ProvHygienist == schedule.ProvNum);
                }
                if (PrefC.HasClinicsEnabled)
                {
                    //Skip this schedule entry if the operatory's clinic does not match the patient's clinic.
                    if (clinic == null)
                    {
                        //If a clinic was not passed in, ONLY consider unassigned operatories
                        listOpsForSchedule = listOpsForSchedule.FindAll(x => x.ClinicNum == 0);
                    }
                    else
                    {
                        //If a valid clinic was passed in, make sure the operatory has a matching clinic.
                        listOpsForSchedule = listOpsForSchedule.FindAll(x => x.ClinicNum == clinic.ClinicNum);
                    }
                }
                if (listOpsForSchedule.Count == 0)
                {
                    continue;                    //No valid operatories for this schedule.
                }
                log?.WriteLine("schedule: " + schedule.ScheduleNum + "\tlistOpsForSchedule:\r\n\t"
                               + string.Join(",\r\n\t", listOpsForSchedule.Select(x => x.OperatoryNum + " - " + x.Abbrev)), LogLevel.Verbose);
                //The list of operatories has been filtered above so we need to find ALL available time slots for this schedule in all operatories.
                foreach (Operatory op in listOpsForSchedule)
                {
                    AddTimeSlotsFromSchedule(listAvailableTimeSlots, schedule, op.OperatoryNum, timeSchedStart, timeSchedStop
                                             , listBlockoutSchedules, dictProvSchedules, listApptsForOps, timePattern, defNumApptType);
                }
            }
            //Remove any time slots that start before right now (just in case the consuming method is looking for slots for today).
            listAvailableTimeSlots.RemoveAll(x => x.DateTimeStart.Date == DateTime.Now.Date && x.DateTimeStart.TimeOfDay < DateTime.Now.TimeOfDay);
            //Order the entire list of available time slots so that they are displayed to the user in sequential order.
            //We need to do this because we loop through each provider's schedule one at a time and add openings as we find them.
            //Then order by operatory.ItemOrder in order to preserve old behavior (filling up the schedule via operatories from the left to the right).
            return(listAvailableTimeSlots.OrderBy(x => x.DateTimeStart)
                   //listOpNums was ordered by ItemOrder at the top of this method so we can trust that it is in the correct order.
                   .ThenBy(x => listOpNums.IndexOf(x.OperatoryNum))
                   .ToList());
        }
        ///<summary>Uses ListApptSinceSignalCache to determine what Appointments and Schedules have changed for the given ClinicNum's ops.
        ///Only goes to the database if an appointment, schedule, or operatory signal is linked to the op's passed in and is in our daterange.
        ///If an entry for a given operatory is included in the output, then all appointments and blockouts for that operatory will be included for all dates specified in the range.
        ///timeRefreshed is the last time signals were processed. A value of DateTime.MinVal will return an empty list of appointments and schedules.
        ///If includeApptItemsInOutput==true then lists of Appointments and Schedules will be full and valid.
        ///When includeApptItemsInOutput==false then lists will be empty and caller is only interesting in whether or not the list of tuples itself would be > 0. This will be used to decide if signal processing should be performed.</summary>
        public static List <Tuple <long, DateTime, List <Appointment>, List <Schedule> > > GetApptsAndSchedsSince(DateTime timeRefreshed, DateTime dateViewingStart, DateTime dateViewingEnd, List <long> listClinicNums, bool includeApptItemsInOutput)
        {
            //Xam will pass DateTime.MinVal if first this is the first attempt for the session.
            //Pass back empty list here so we can establish TimeRefreshed for next time.
            if (timeRefreshed == DateTime.MinValue)
            {
                return(new List <Tuple <long, DateTime, List <Appointment>, List <Schedule> > >());
            }
            List <Signalod> listSignalsFiltered = null;

            lock (_lockInvalidTypes) {
                listSignalsFiltered = ListApptSinceSignalCache.FindAll(x =>
                                                                       //Only care about signal since the last time we asked.
                                                                       x.SigDateTime >= timeRefreshed &&
                                                                       //Always include signals that have not specified DateViewing of MinVal. This is a special case means a full appt refresh is needed.
                                                                       //Otherwise only include signals which match that date range that we are actually viewing right now.
                                                                       (x.DateViewing == DateTime.MinValue.Date || x.DateViewing.Between(dateViewingStart, dateViewingEnd, true, true)));
            }
            List <Signalod> listApptSignals  = listSignalsFiltered.FindAll(x => x.IType == InvalidType.Appointment);
            List <Signalod> listSchedSignals = listSignalsFiltered.FindAll(x => x.IType == InvalidType.Schedules);
            //Operatories signal insert is not accompanied by an Appointment signal in OD proper.
            //So if we get any Op signal, assume that all appts are dirty for all clinics. Dirty Ops is rare so it's fine to be overly cautious.
            bool isOpsChanged = listSignalsFiltered.Any(x => x.IType == InvalidType.Operatories);
            //Get all operatories for each clinic in listClinics, generally we will only be passed one clinic.
            List <long> listOpNumsVisible = new List <long>();

            foreach (long clinicNum in listClinicNums)
            {
                //For now, all ops for this clinic. If we ever implement appt view for OD Mobile, we will need a filter here.

                //todo: test with clinicNum 0 and clinics off. Should get all clinics but probably only gets clinicNum 0.

                listOpNumsVisible.AddRange(Operatories.GetOpsForClinic(clinicNum).Select(x => x.OperatoryNum).ToList());
            }
            //Remove duplicates.
            listOpNumsVisible = listOpNumsVisible.Distinct().ToList();
            //Init these to empty list, not null. This is important down below.
            List <Appointment> listAppts  = new List <Appointment>();
            List <Schedule>    listScheds = new List <Schedule>();
            List <Tuple <long, DateTime, List <Appointment>, List <Schedule> > > ret = new List <Tuple <long, DateTime, List <Appointment>, List <Schedule> > >();

            if (
                //An Operatory signal means we should refresh all operatories for all dates.
                isOpsChanged
                //An appt signal where we don't know exactly which appts have changed so refresh all operatories.
                || listApptSignals.Exists(x => x.DateViewing == DateTime.MinValue.Date)
                //A schedule signal where we don't know exactly which schedules have changed so refresh all blockouts.
                || listSchedSignals.Exists(x => x.DateViewing == DateTime.MinValue.Date))
            {
                //Only query the db when we absolutely have to.
                if (includeApptItemsInOutput && listOpNumsVisible.Count > 0)
                {
                    //Get all appts and schedules for the ops which have changes.
                    listAppts  = Appointments.GetAppointmentsForOpsByPeriod(listOpNumsVisible, dateViewingStart, dateViewingEnd);
                    listScheds = Schedules.GetAllForDateRangeAndType(dateViewingStart, dateViewingEnd, ScheduleType.Blockout, false, listOpNumsVisible);
                }
                //One entry in the list per every operatory and date in our range.
                foreach (long opNum in listOpNumsVisible)
                {
                    int days = dateViewingEnd.Subtract(dateViewingStart).Days;
                    for (int i = 0; i <= days; i++)
                    {
                        //The entry may be empty for a given operatory if there are currently no items for the given date range.
                        DateTime date = dateViewingStart.AddDays(i).Date;
                        ret.Add(new Tuple <long, DateTime, List <Appointment>, List <Schedule> >(
                                    opNum,
                                    date,
                                    //Will be empty in !includeApptItemsInOutput.
                                    listAppts.FindAll(y => y.Op == opNum && y.AptDateTime.Date == date),
                                    //Will be empty in !includeApptItemsInOutput.
                                    listScheds.FindAll(y => y.Ops.Contains(opNum) && y.SchedDate.Date == date)
                                    ));
                    }
                }
            }
            else
            {
                //Only add entries for operatory/date pairs which had a signal specificially added.
                var dateChanges = listApptSignals
                                  //We are not considering KeyType.Provider for now.
                                  //This leaves a tiny edge case where a KeyType.Provider signal was made by SignalOds.SetInvalidAppt() but not a KeyType.Operatory signal.
                                  //Sam and Luke were not even able to produce this edge case and implementing it would cause us to have to redesign this linq statement to not be Ops-based.
                                  //If a bug is reported in the future regarding changing something about provider not updating the appt view in ODMobile then this is likely the culprit.
                                  .FindAll(x => x.FKeyType == KeyType.Operatory && x.FKey.In(listOpNumsVisible))
                                  .GroupBy(x => new { x.FKey, x.DateViewing.Date })
                                  .Select(x => new { Op = x.Key.FKey, x.Key.Date })
                                  .Union(listSchedSignals
                                         .FindAll(x => x.FKeyType == KeyType.Operatory && x.FKey.In(listOpNumsVisible))
                                         .GroupBy(x => new { x.FKey, x.DateViewing.Date })
                                         .Select(x => new { Op = x.Key.FKey, x.Key.Date })
                                         )
                                  .GroupBy(x => new { x.Op, x.Date })
                                  .Select(x => new { x.Key.Op, x.Key.Date })
                                  .GroupBy(x => x.Date)
                                  .Select(x => new { Date = x.Key, Ops = x.Select(y => y.Op).ToList() });
                foreach (var dateChange in dateChanges)
                {
                    //Only query the db when we absolutely have to.
                    if (includeApptItemsInOutput)
                    {
                        //Get all appts and schedules for each unique op/date.
                        //Some dates in our range may have changes and some may not so we will need to query the db once for each date which has changes.
                        //Appointments.GetAppointmentsForOpsByPeriod DOES add a day to DateEnd.
                        listAppts = Appointments.GetAppointmentsForOpsByPeriod(dateChange.Ops, dateChange.Date, dateChange.Date);
                        //Schedules.GetAllForDateRangeAndType does NOT add a day to DateEnd.
                        listScheds = Schedules.GetAllForDateRangeAndType(dateChange.Date, dateChange.Date.AddDays(1), ScheduleType.Blockout, false, dateChange.Ops);
                    }
                    //One entry in the list per changed operatory/date. The entry may be empty for a given operatory if there are currently no items for the given date range.
                    ret.AddRange(dateChange.Ops.Select(x => new Tuple <long, DateTime, List <Appointment>, List <Schedule> >(
                                                           x,
                                                           dateChange.Date,
                                                           //Will be empty in !includeApptItemsInOutput.
                                                           listAppts.FindAll(y => y.Op == x && y.AptDateTime.Date == dateChange.Date),
                                                           //Will be empty in !includeApptItemsInOutput.
                                                           listScheds.FindAll(y => y.Ops.Contains(x) && y.SchedDate.Date == dateChange.Date)
                                                           )));
                }
            }
            return(ret);
        }