function calendar_get_available_faculty_schedule

7.x calendar.module calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year)
6.x calendar.module calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year)

Returns back an array of time slots available for this faculty member and event_type

We will convert all times to local timezone as well.

2 calls to calendar_get_available_faculty_schedule()
calendar_display_schedule_appointment_page in modules/calendar/calendar.module
This is the page which lets students schedule an appointment with the faculty member supplied in the user_id.
calendar_schedule_appointment_confirm_form_validate in modules/calendar/calendar.module
This is our last chance to validate the form before saving.

File

modules/calendar/calendar.module, line 1414

Code

function calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year) {

  $rtn = array();

  $event_type_content = content_load($event_type_cid);
  // Confirm that the faculty_id in this content matches our supplied faculty_id.  
  if ($event_type_content->field__faculty_id ['value'] != $faculty_id) {
    fp_add_message(t("Invalid schedule link."));
    fp_goto("<front>");
    return;
  }

  $prevent_less_than_hours = intval($event_type_content->field__prevent_less_than_hours ['value']);

  // Get this faculty user's timezone.
  $faculty_user_id = db_get_user_id_from_cwid($faculty_id);
  $tz = fp_get_user_timezone($faculty_user_id);

  // Get all of our unavailable times into a single array, organized by days.
  $unavailable_times = content_get_content_for_faculty_id('schedule_unavailable_time', $faculty_id);
  $unavailable_by_days = array();
  foreach ($unavailable_times as $ut_content) {
    // If this is an ICS link, then skip for now.
    if (trim($ut_content->field__ics_url ['value']) != '') {
      continue;
    }

    $time_selector = trim($ut_content->field__time_selector ['value']);
    if ($time_selector == "") {
      $time_selector = "default";
    }

    foreach ($ut_content->field__days ['value'] as $dow) {



      $start_end_array = array();

      if ($time_selector == "manual") {
        $st = $ut_content->field__start_time ['value'];
        $et = $ut_content->field__end_time ['value'];

        $start_end_array [] = array(
          'start' => $st,
          'end' => $et,
          'convert_tz' => TRUE, // since these are in UTC, they will indeed need to be converted.
        );

      } // manual
      else if ($time_selector == "default") {

        // We will construct times based on the day_start_hour and stop_hour          
        $st = $ut_content->field__day_start_hour ['value'] . ":00:00";
        $et = $ut_content->field__day_stop_hour ['value'] . ":00:00";

        if ($et == "0:00:00") {
          $et = "23:59:59"; // meaning, we are stopping at midnight
        }


        // We want to calculate out what our start time is LESS 1 second, so that we can begin accepting appointments
        // on the time we selected.
        $st_less_one_sec = $st; // default
        $start_hour = intval($ut_content->field__day_start_hour ['value']);
        if ($start_hour > 0) {
          $st_less_one_sec = ($start_hour - 1) . ":59:59";
        }

        $et_plus_one_sec = $et; // default
        $stop_hour = intval($ut_content->field__day_stop_hour ['value']);
        if ($stop_hour < 23) {
          $et_plus_one_sec = ($stop_hour + 1) . ":00:01";
        }



        // The first one is the start.  We go from midnight to $st minus 1 second.
        $start_end_array [] = array(
          'start' => "00:00:00",
          'end' => $st_less_one_sec,
          'convert_tz' => FALSE, // these times are in the faculty member's time zone.  They'll NOT need to be converted to UTC in the next step.
        );

        // The second one is the end.  We go from $et (plus one second) to 23:59:59
        $start_end_array [] = array(
          'start' => $et_plus_one_sec,
          'end' => "23:59:59",
          'convert_tz' => FALSE, // these times are in the faculty member's time zone.  They'll NOT need to be converted to UTC in the next step.
        );

      } // default
      else if ($time_selector == "none") {
        // The user doesn't work at all on the selected days.
        // We go from 00:00:00 to 23:59:59
        $start_end_array [] = array(
          'start' => "00:00:00",
          'end' => "23:59:59",
          'convert_tz' => FALSE, // these times are in the faculty member's time zone.  They'll NOT need to be converted to UTC in the next step.
        );

      } // none



      // Now, populate our unavailable array          
      foreach ($start_end_array as $c => $details) {

        $st = $details ['start'];
        $et = $details ['end'];
        $convert_tz = $details ['convert_tz'];

        // st and et are in UTC.  The original faculty user's timezone is tz.
        // Let's first convert them to the ORIGINAL timezone FROM UTC.

        $sample_date = '2020-01-01';

        // Grab the UTC times.  Since we only care about hours/minutes, it's OK that we use the same date.
        $utc_st = strtotime($sample_date . " " . $st);
        $utc_et = strtotime($sample_date . " " . $et);

        $tz_st = $utc_st;
        $tz_et = $utc_et;
        if ($convert_tz == TRUE) {
          // Convert the UTC time into faculty member's timezone
          $tz_st = convert_time($utc_st, 'UTC', $tz);
          $tz_et = convert_time($utc_et, 'UTC', $tz);
        }
        // The start and end times are now in the original faculty member's timezone.
        $unavailable_by_days [$dow][] = array(
          'start_time' => date("H:i", $tz_st),
          'end_time' => date("H:i", $tz_et),
          'tz' => $tz,
        );
      }



    } // foreach field__days


  } // foreach unavailable_times  

  // Add to the unavailable_by_days array:
  // existing appointments on our FP calendar



  $existing_appointments = calendar_get_appointments_for_faculty($faculty_id, date('Y-m-d', strtotime("NOW - 24 HOURS")), date('Y-m-d', strtotime('NOW + 4 MONTHS')), TRUE);

  // Okay, we need to find out how long this event lasts + any buffer time.
  // how_long_min is basically when we can create our "slots" for the user to select.  So if the number is 20 minutes, and we start at 11:30am, then
  // we'd see a slot at:  11:30am, 11:50am, 12:10pm, 12:30pm, and so on.

  $how_long_min = intval($event_type_content->field__event_duration_minutes ['value']) + intval($event_type_content->field__event_buffer_minutes ['value']);
  $how_long_sec = $how_long_min * 60;

  // Next, we are going to go through every single day of this month, to find out what all our slots are.
  $total_days = cal_days_in_month(CAL_GREGORIAN, $month, $year);

  for ($day = 1; $day <= $total_days; $day++) {

    $begin_time = "00:00:00";
    $end_time = "23:59:59";

    $skip_begin_slot = array();

    // We DO NOT NEED TO CONVERT for this!    
    $begin_time_ts = strtotime("$year-$month-$day $begin_time");
    $end_time_ts = strtotime("$year-$month-$day $end_time");

    $day_of_week = intval(date('w', $begin_time_ts));

    // Loop from our begin time to our end time, by how_long_sec each step.  We then
    // want to see if there were any conflicts.  If not, then it's safe to permit that time slot in our $rtn array
    // for this day.
    $begin_slot_ts = $begin_time_ts;
    for ($end_slot_ts = $begin_time_ts + $how_long_sec; $end_slot_ts <= $end_time_ts; $end_slot_ts += $how_long_sec) {


      // Skip if it's before X hours from now, based on the event_type_content. This will also catch all the days that have already passed.    
      if ($begin_slot_ts < strtotime("NOW + $prevent_less_than_hours HOURS")) {

        // get ready for next time through the loop      
        $begin_slot_ts = $end_slot_ts;
        continue;
      }

      // Okay--  We have a slot of time-- from begin_slot_ts to end_slot_ts.
      // check to see if there are any conflicts during this "slot".

      $bool_conflict = FALSE;
      // check our unavailable times
      if (isset($unavailable_by_days [$day_of_week])) {
        foreach ($unavailable_by_days [$day_of_week] as $details) {
          $un_start_time = $details ['start_time']; // These are now in their original timezone!
          $un_end_time = $details ['end_time']; // these are now in their original timezone!

          $un_start_time_ts = strtotime("$year-$month-$day $un_start_time");
          $un_end_time_ts = strtotime("$year-$month-$day $un_end_time");



          // Now, does our "slot" conflict with this unavailable time window?

          // Scenario #1:  Our unavailable time completely engulfs our slot.
          // Ex:  { unavail     [ slot ]      }
          if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $end_slot_ts) {
            $bool_conflict = TRUE;
            break;
          }

          // Scenario #2:  Our slot completely engulfs our unavailable time window.
          // Ex:  [ slot     { unavail }      ]
          if ($begin_slot_ts <= $un_start_time_ts && $end_slot_ts >= $un_end_time_ts) {
            $bool_conflict = TRUE;
            break;
          }

          // Scenario #3:  Our unavail time starts before slot, but ends inside of the slot.
          // Ex:  { unavail  [ slot  }    ]      
          if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $begin_slot_ts && $un_end_time_ts <= $end_slot_ts) {
            $bool_conflict = TRUE;
            break;
          }

          // Scenario #4:  Our unavail time starts after slot begins
          // Ex:  [ slot         { unavail     ]    }      
          if ($un_start_time_ts >= $begin_slot_ts && $un_start_time_ts <= $end_slot_ts && $un_end_time_ts >= $end_slot_ts) {
            $bool_conflict = TRUE;
            break;
          }

        } // foreach unavailable by days[dow]
      }


      // Okay, now check our existing_appointments, and see if there are any conflicts thee.
      if (isset($existing_appointments [$year][$month][$day])) {
        foreach ($existing_appointments [$year][$month][$day] as $appt) {
          $un_start_time_ts = $appt ['start_ts']; // These are in UTC
          $un_end_time_ts = $appt ['end_ts'];


          // For reasons that aren't completely known to me, here we are going to add the time-converted
          // begin_slot_ts to an array, which we'll use later to "prune" our return results.



          // Now, does our "slot" conflict with this unavailable time window?          
          // Scenario #1:  Our unavailable time completely engulfs our slot.
          // Ex:  { unavail     [ slot ]      }
          if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $end_slot_ts) {
            $bool_conflict = TRUE;
            $skip_begin_slot [] = convert_time($begin_slot_ts, 'UTC', $tz);
            break;
          }

          // Scenario #2:  Our slot completely engulfs our unavailable time window.
          // Ex:  [ slot     { unavail }      ]
          if ($begin_slot_ts <= $un_start_time_ts && $end_slot_ts >= $un_end_time_ts) {
            $bool_conflict = TRUE;
            $skip_begin_slot [] = convert_time($begin_slot_ts, 'UTC', $tz);
            break;
          }

          // Scenario #3:  Our unavail time stars before slot, but ends inside of the slot.
          // Ex:  { unavail  [ slot  }    ]      
          if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $begin_slot_ts && $un_end_time_ts <= $end_slot_ts) {
            $bool_conflict = TRUE;

            $skip_begin_slot [] = convert_time($begin_slot_ts, 'UTC', $tz);
            break;
          }

          // Scenario #4:  Our unavail time stars after slot begins
          // Ex:  [ slot         { unavail     ]    }      
          if ($un_start_time_ts >= $begin_slot_ts && $un_start_time_ts <= $end_slot_ts && $un_end_time_ts >= $end_slot_ts) {
            $bool_conflict = TRUE;
            $skip_begin_slot [] = convert_time($begin_slot_ts, 'UTC', $tz);
            break;

          }




        } // foreach existing appt
      } // if isset

      ////////////
      // If we made it here, then there are no conflicts.
      // Let's add it in to our rtn array.
      if ($bool_conflict == FALSE) {
        $begin_hm = date('g:ia', $begin_slot_ts);
        $rtn [$year][$month][$day][$begin_slot_ts] = array(
          'begin_slot_ts' => $begin_slot_ts,
          'end_slot_ts' => $end_slot_ts,
          'begin_hm' => $begin_hm,
          'end_hm' => date('g:ia', $end_slot_ts),
          'tz' => $tz,
          'how_long_min' => $how_long_min,
          'how_long_sec' => $how_long_sec,
        );
      }



      // get ready for next time through the loop      
      $begin_slot_ts = $end_slot_ts;
    } // for end_slot_ts



    // Prune our rtn array based on the skip_begin_slot array.  I know this is an inefficient hack.  I am being
    // slowly driven crazy by this timezone shit.  
    if (isset($rtn [$year][$month][$day])) {
      foreach ($rtn [$year][$month][$day] as $bg => $deets) {
        if (in_array($bg, $skip_begin_slot)) {
          unset($rtn [$year][$month][$day][$bg]);
        }
      }
    }

  } // for day




  return $rtn;

}