calendar.module

File

modules/calendar/calendar.module
View source
  1. <?php
  2. /**
  3. * implements hook_menu
  4. */
  5. function calendar_menu() {
  6. $items = array();
  7. $items['calendar'] = array(
  8. 'title' => 'Appointments',
  9. 'page_callback' => 'calendar_display_calendar',
  10. 'access_arguments' => array('access_logged_in_content'),
  11. 'type' => MENU_TYPE_TAB,
  12. 'tab_family' => 'calendar_tabs',
  13. "weight" => 10,
  14. );
  15. $items['calendar/upcoming'] = array(
  16. 'title' => 'Upcoming Appointments',
  17. 'page_callback' => 'calendar_display_upcoming_appointments',
  18. 'access_arguments' => array('access_logged_in_content'),
  19. 'type' => MENU_TYPE_TAB,
  20. 'tab_family' => 'calendar_tabs',
  21. "weight" => 15,
  22. );
  23. $items['calendar/schedule-staff'] = array(
  24. 'title' => 'Schedule Appointments',
  25. 'page_callback' => 'calendar_display_schedule_staff_page',
  26. 'access_callback' => 'fp_user_is_student',
  27. 'type' => MENU_TYPE_TAB,
  28. 'tab_family' => 'calendar_tabs',
  29. "weight" => 50,
  30. );
  31. //calendar/confirm-cancel-appointment/$content->cid
  32. $items['calendar/confirm-cancel-appointment/%'] = array(
  33. 'title' => 'Confirm Cancel Appointment',
  34. 'page_callback' => 'fp_render_form',
  35. 'page_arguments' => array('calendar_confirm_cancel_appointment_form', '', 2),
  36. 'access_callback' => 'calendar_access_can_cancel_appointment',
  37. 'access_arguments' => array(2),
  38. );
  39. $items['schedule-appointment/%'] = array(
  40. 'title' => 'Schedule Appointment',
  41. 'page_callback' => 'calendar_display_schedule_appointment_page',
  42. 'page_arguments' => array(1),
  43. 'access_callback' => TRUE,
  44. 'type' => MENU_TYPE_NORMAL_ITEM,
  45. );
  46. $items['schedule-appointment/%/confirm'] = array(
  47. 'title' => 'Confirm Appointment',
  48. 'page_callback' => 'fp_render_form',
  49. 'page_arguments' => array('calendar_schedule_appointment_confirm_form', 'normal', 1),
  50. 'access_callback' => TRUE,
  51. 'type' => MENU_TYPE_NORMAL_ITEM,
  52. );
  53. $items['schedule-appointment-completed/%'] = array(
  54. 'title' => t('Appointment Scheduled Successfully'),
  55. 'page_callback' => 'calendar_display_schedule_appointment_completed_page',
  56. 'page_arguments' => array(1),
  57. 'access_callback' => TRUE,
  58. 'type' => MENU_TYPE_NORMAL_ITEM,
  59. );
  60. $items['user-settings/appointment-settings'] = array(
  61. 'title' => t('Appointment Settings'),
  62. 'page_callback' => 'calendar_display_user_appointment_settings_page',
  63. 'access_arguments' => array('can_accept_appointments'),
  64. 'type' => MENU_TYPE_TAB,
  65. 'tab_family' => 'user_settings',
  66. 'weight' => 10,
  67. );
  68. $items['admin/config/appointments'] = array(
  69. 'title' => 'Appointment settings',
  70. 'description' => 'Configure global appointment settings',
  71. 'page_callback' => 'fp_render_form',
  72. 'page_arguments' => array('calendar_appointment_settings_form', 'system_settings'),
  73. 'access_arguments' => array('administer_appointment_settings'),
  74. "page_settings" => array(
  75. "page_show_title" => TRUE,
  76. "menu_icon" => fp_get_module_path('system') . "/icons/calendar_view_day.png",
  77. "menu_links" => array(
  78. 0 => array(
  79. "text" => "Admin Console",
  80. "path" => "admin-tools/admin",
  81. "query" => "de_catalog_year=%DE_CATALOG_YEAR%",
  82. ),
  83. ),
  84. ),
  85. 'type' => MENU_TYPE_NORMAL_ITEM,
  86. "tab_parent" => "admin-tools/admin",
  87. );
  88. return $items;
  89. } // hook_menu
  90. /**
  91. * Lets an admin user configure global settings regarding appointments in FlightPath
  92. */
  93. function calendar_appointment_settings_form($school_id = 0) {
  94. $form = array();
  95. // Similar to other forms, this one must be separated out by school. See the course search settings.
  96. $school_id = intval($school_id);
  97. $fs = ""; // The field name suffix. We will add this to the end of all of our field names. If this is the default school, leave blank.
  98. if (module_enabled("schools")) {
  99. $school_name = schools_get_school_name_for_id($school_id);
  100. fp_set_title(t("Configure %school Appointment settings", array('%school' => $school_name)));
  101. if ($school_id !== 0) {
  102. $fs = "~~school_" . $school_id;
  103. }
  104. }
  105. $form['school_id'] = array(
  106. 'type' => 'hidden',
  107. 'value' => $school_id,
  108. );
  109. $options = fp_get_departments();
  110. $form['appointments_from_departments' . $fs] = array(
  111. 'type' => 'checkboxes',
  112. 'label' => t("Select departments which allow any of their employees to appear on the student Schedule Appointment screen (for every student):"),
  113. 'options' => $options,
  114. 'value' => variable_get_for_school('appointments_from_departments', array(), $school_id, TRUE),
  115. );
  116. return $form;
  117. } // calendar_appointment_settings_form
  118. /**
  119. * This tab displays the upcoming appointments for the user. It's meant to be a tab on their Calendar screen.
  120. */
  121. /*
  122. function z__calendar_display_upcoming_appointments() {
  123. global $user;
  124. fp_add_css(fp_get_module_path("system") . "/css/style.css");
  125. $render = array();
  126. $render['#id'] = 'calendar_display_upcoming_appointments';
  127. // Build up the "appoinments" HTML
  128. $render['mark_top'] = array(
  129. 'value' => "<h4>" . t("Your upcoming appointments/agenda over the next few weeks:") . "</h4><hr>",
  130. );
  131. $user_tz = fp_get_user_timezone();
  132. $ranges = array(
  133. "today" => array(strtotime("NOW $user_tz"), strtotime("TOMORROW - 1 SECOND $user_tz")),
  134. "tomorrow" => array(strtotime("TOMORROW $user_tz"), strtotime("NOW + 1 DAY $user_tz")),
  135. "this week" => array(strtotime("NOW + 1 DAY $user_tz"), strtotime("NEXT MONDAY - 1 SECOND $user_tz")),
  136. "next 2 weeks" => array(strtotime("NEXT MONDAY $user_tz"), strtotime("NEXT MONDAY + 15 DAYS $user_tz")),
  137. );
  138. $bool_is_empty = TRUE;
  139. $c = 0;
  140. foreach ($ranges as $when => $details) {
  141. $when_desc = t("Today");
  142. if ($when == "tomorrow") $when_desc = t("Tomorrow");
  143. if ($when == "this week") $when_desc = t("Remaining days this week");
  144. if ($when == "next 2 weeks") $when_desc = t("Next few weeks");
  145. $start_ts = $details[0];
  146. $end_ts = $details[1];
  147. $start_time = date("Y-m-d H:i:s", $start_ts);
  148. $end_time = date("Y-m-d H:i:s", $end_ts);
  149. $upcoming = calendar_get_upcoming_appointments_for_cwid($user->cwid, $start_time, $end_time);
  150. if ($upcoming && count($upcoming) > 0) {
  151. $render['mark_' . fp_get_machine_readable($when)] = array(
  152. 'value' => "<h4>$when_desc</h4>",
  153. );
  154. }
  155. else {
  156. continue;
  157. }
  158. foreach ($upcoming as $details) {
  159. $thedate = format_date(convert_time($details['utc_start_ts']), 'long_no_year');
  160. $use_name = $details['faculty_name'];
  161. if ($user->is_faculty) {
  162. $use_name = $details['student_name'];
  163. }
  164. $bool_is_empty = FALSE;
  165. $msg = t("You have an appointment with @fn on @td.", array("@fn" => $use_name, "@td" => $thedate));
  166. $render['appointment_' . $c] = array(
  167. 'value' => "<div class='feed-item'>
  168. <div class='feed-item-icon'><i class='fa fa-calendar'></i></div>
  169. <div class='feed-item-title'>$use_name</div>
  170. <div class='feed-item-desc'>$msg</div>
  171. </div>",
  172. );
  173. $c++;
  174. } // foreach
  175. } //foreach ranges
  176. if ($bool_is_empty) {
  177. $render['no_appointments'] = array(
  178. 'value' => "<div class='empty'>
  179. <p>" . t("You have no upcoming appointments within the next few weeks.") . "</p>
  180. </div>",
  181. );
  182. }
  183. return fp_render_content($render);
  184. } // z__calendar_display_upcoming_appointments
  185. */
  186. function calendar_display_upcoming_appointments() {
  187. global $user;
  188. fp_add_css(fp_get_module_path("system") . "/css/style.css");
  189. fp_set_title('');
  190. $render = array();
  191. $render['#id'] = 'calendar_display_upcoming_appointments';
  192. // Build up the "appoinments" HTML
  193. $render['mark_top'] = array(
  194. 'value' => "<h4>" . t("Your upcoming appointments/agenda over the next few weeks:") . "</h4><hr>",
  195. );
  196. $user_tz = fp_get_user_timezone();
  197. $bool_is_empty = TRUE;
  198. $c = 0;
  199. $start_ts = convert_time(time());
  200. $end_ts = strtotime("NOW + 21 DAYS");
  201. $start_time = date("Y-m-d H:i:s", $start_ts);
  202. $end_time = date("Y-m-d H:i:s", $end_ts);
  203. $upcoming = calendar_get_upcoming_appointments_for_cwid($user->cwid, $start_time, $end_time);
  204. $today = date('Y-m-d', $start_ts);
  205. $tomorrow = date('Y-m-d', $start_ts + (24 * 60 * 60)); // 24 hours later
  206. $next_few_weeks_ts = $start_ts + (24 * 60 * 60) + 1;
  207. $current_range = "";
  208. foreach ($upcoming as $details) {
  209. $thedate = format_date(convert_time($details['utc_start_ts']), 'long_no_year');
  210. // Figure out of this is TODAY, TOMORROW, or over the next few weeks.
  211. $ymd = date('Y-m-d', convert_time($details['utc_start_ts']));
  212. if ($ymd == $today && $current_range != "today") {
  213. $render['mark_today_header'] = array(
  214. 'value' => "<h4>" . t("Today") . "</h4>",
  215. );
  216. $current_range = "today";
  217. }
  218. if ($ymd == $tomorrow && $current_range != "tomorrow") {
  219. $render['mark_tomorrow_header'] = array(
  220. 'value' => "<h4>" . t("Tomorrow") . "</h4>",
  221. );
  222. $current_range = "tomorrow";
  223. }
  224. //if (convert_time($details['utc_start_ts']) >= $next_few_weeks_ts && $current_range != "next_few") {
  225. if ($ymd != $today && $ymd != $tomorrow && $current_range != "next_few") {
  226. $render['mark_next_few_header'] = array(
  227. 'value' => "<h4>" . t("Next few weeks") . "</h4>",
  228. );
  229. $current_range = "next_few";
  230. }
  231. $use_name = $details['faculty_name'];
  232. if ($user->is_faculty) {
  233. $use_name = $details['student_name'];
  234. }
  235. $bool_is_empty = FALSE;
  236. $msg = t("You have an appointment with @fn on @td.", array("@fn" => $use_name, "@td" => $thedate));
  237. $render['appointment_' . $c] = array(
  238. 'value' => "<div class='feed-item'>
  239. <div class='feed-item-icon'><i class='fa fa-calendar'></i></div>
  240. <div class='feed-item-title'>$use_name</div>
  241. <div class='feed-item-desc'>$msg</div>
  242. </div>",
  243. );
  244. $c++;
  245. } // foreach
  246. if ($bool_is_empty) {
  247. $render['no_appointments'] = array(
  248. 'value' => "<div class='empty'>
  249. <p>" . t("You have no upcoming appointments within the next few weeks.") . "</p>
  250. </div>",
  251. );
  252. }
  253. watchdog("calendar", "display_upcoming", array());
  254. return fp_render_content($render);
  255. } // calendar_display_upcoming_appointments
  256. /**
  257. * Make sure the user is allowed to cancel this appointment.
  258. *
  259. * Used by the hook_menu item
  260. */
  261. function calendar_access_can_cancel_appointment($cid) {
  262. global $user;
  263. $content = content_load($cid);
  264. if (user_has_permission("can_cancel_own_appointments")) {
  265. if ($user->cwid == $content->field__student_id['value'] || $user->cwid == $content->field__faculty_id['value'] || $user->id == 1) {
  266. return TRUE;
  267. }
  268. }
  269. return FALSE;
  270. }
  271. /**
  272. * Confirm we actually want to cancel this appointment
  273. */
  274. function calendar_confirm_cancel_appointment_form($cid) {
  275. $form = array();
  276. $content = content_load($cid);
  277. fp_add_css(fp_get_module_path("content") . "/css/content.css");
  278. $types = content_get_types();
  279. $type = $content->type;
  280. // Go back to the original content (our default option)
  281. $form['#redirect'] = array('path' => "content/$cid");
  282. // We want to redirect correctly based on this type's options....
  283. // Are there any special settings for the title?
  284. if (isset($types[$type]['settings'])) {
  285. if (is_array($types[$type]['settings']['#redirect'])) {
  286. $form['#redirect'] = $types[$type]['settings']['#redirect'];
  287. }
  288. }
  289. $student_name = @fp_get_student_name($content->field__student_id['value'], TRUE);
  290. $faculty_name = @fp_get_faculty_name($content->field__faculty_id['value']);
  291. // Make a nice title for this appointment.
  292. $apt_title = t("Meeting between @ft and @st on @dt", array("@ft" => $faculty_name, "@st" => $student_name, "@dt" => $content->field__appointment_datetime['display_value']));
  293. $form['cid'] = array(
  294. 'type' => 'hidden',
  295. 'value' => $cid,
  296. );
  297. $form['mark_top'] = array(
  298. 'type' => 'markup',
  299. 'value' => "<h2>" . t("Are you sure you wish to cancel %ap?", array("%ap" => $apt_title)) . "</h2>
  300. <p>" . t("This action will send a notification to both parties that the appointment has been canceled.") . "</p>
  301. <hr>",
  302. );
  303. //If this appointment had meeting data (zoom) then we should include some text here that explains it will be cancelled as well.
  304. if (isset($content->field__video_data) && isset($content->field__video_data['value']) && $content->field__video_data['value'] != "") {
  305. $data = json_decode($content->field__video_data['value']);
  306. $video_provider = $data->video_provider;
  307. if ($video_provider == "zoom" && module_enabled('zoomapi')) {
  308. $id = $data->id;
  309. $form['video_msg_markup'] = array(
  310. 'type' => 'markup',
  311. 'value' => t("<strong>Zoom Meeting:</strong> By cancelling this appointment, the scheduled Zoom meeting (ID: $id) will also be cancelled."),
  312. );
  313. }
  314. }
  315. $form['msg'] = array(
  316. 'type' => 'textarea',
  317. 'label' => t("Reason"),
  318. 'description' => t("Please enter a brief reason for canceling this appointment."),
  319. );
  320. $form['submit_confirm'] = array(
  321. 'type' => 'submit',
  322. 'spinner' => TRUE,
  323. 'value' => t('Confirm & Cancel Appointment'),
  324. 'attributes' => array('class' => 'content-submit-btn'),
  325. 'suffix' => "&nbsp; &nbsp; <a href='javascript:history.go(-1);'>" . t("Cancel") . "</a>",
  326. );
  327. return $form;
  328. } // confirm_cancel_appointment_form
  329. function calendar_confirm_cancel_appointment_form_submit(&$form, &$form_state) {
  330. global $user;
  331. if ($user->is_student) {
  332. $user_name = fp_get_student_name($user->cwid, FALSE);
  333. }
  334. else {
  335. $user_name = fp_get_faculty_name($user->cwid);
  336. }
  337. if (isset($form['#redirect'])) {
  338. fp_add_message(t("The appointment was cancelled."));
  339. $form['#redirect']['query'] .= "&cancel=yes";
  340. }
  341. else {
  342. // No redirect was specified, so let's just return.
  343. fp_add_message(t("The appointment was cancelled."));
  344. }
  345. $cid = $form_state['values']['cid'];
  346. $content = content_load($cid);
  347. $reason_msg = trim(filter_markup($form_state['values']['msg'], 'plain'));
  348. $utc_appointment_datetime = strtotime($content->field__appointment_datetime['value']);
  349. $student_cwid = $content->field__student_id['value'];
  350. $faculty_cwid = $content->field__faculty_id['value'];
  351. // If the appointment had meeting data (eg, Zoom), make sure we send a request to delete the Zoom meeting as well.
  352. if (isset($content->field__video_data) && isset($content->field__video_data['value']) && $content->field__video_data['value'] != "") {
  353. $data = json_decode($content->field__video_data['value']);
  354. $video_provider = $data->video_provider;
  355. if ($video_provider == "zoom") {
  356. $id = $data->id;
  357. $video_account_user_id = $data->video_account_owner_user_id;
  358. if (module_enabled("zoomapi")) {
  359. zoomapi_cancel_zoom_meeting($id, $video_account_user_id);
  360. }
  361. }
  362. }
  363. // First, send a notification to the student.
  364. $student_user_id = db_get_user_id_from_cwid($student_cwid, 'student');
  365. $student_account = fp_load_user($student_user_id);
  366. $student_tz = fp_get_user_timezone($student_account);
  367. $apt_date = format_date(convert_time($utc_appointment_datetime, 'UTC', $student_tz), 'short');
  368. $msg = "APPOINTMENT CANCELED: $user_name has canceled appointment: $apt_date";
  369. if ($reason_msg) {
  370. $msg .= "\n\n<br><br>--- Reason given: " . $reason_msg;
  371. }
  372. notify_send_notification_to_user($student_user_id, $msg, $cid, 'appointment');
  373. // Now, send message to faculty member
  374. $faculty_user_id = db_get_user_id_from_cwid($faculty_cwid, 'faculty');
  375. $faculty_account = fp_load_user($faculty_user_id);
  376. $faculty_tz = fp_get_user_timezone($faculty_account);
  377. $apt_date = format_date(convert_time($utc_appointment_datetime, 'UTC', $faculty_tz), 'short');
  378. $msg = "APPOINTMENT CANCELED: $user_name has canceled appointment: $apt_date";
  379. if ($reason_msg) {
  380. $msg .= "\n\n<br><br>--- Reason given: " . $reason_msg;
  381. }
  382. notify_send_notification_to_user($faculty_user_id, $msg, $cid, 'appointment');
  383. // Okay, now we set the content to be "unpublished" and save.
  384. $content->published = 0;
  385. content_save($content);
  386. watchdog("calendar", "appointment_cancel cid:$cid", array());
  387. } // .. confirm_cancel_appointment_form_submit
  388. /**
  389. * This page (primarily meant for students) is for quickly finding
  390. * your advisor or professor or whomever, and finding their link to schedule
  391. * an appointment with them.
  392. */
  393. function calendar_display_schedule_staff_page() {
  394. global $user;
  395. fp_set_title('');
  396. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  397. $db = get_global_database_handler();
  398. if ($user->is_student) {
  399. $your_options = array();
  400. $advisors = advise_get_advisors_for_student($user->cwid);
  401. foreach ($advisors as $c => $faculty_id) {
  402. // Does this faculty member accept appointments?
  403. $event_types = content_get_content_for_faculty_id('schedule_event_type', $faculty_id);
  404. $bool_at_least_one_enabled = FALSE;
  405. foreach ($event_types as $content) {
  406. if ($content->field__enabled['value'] == 'enabled') {
  407. $bool_at_least_one_enabled = TRUE;
  408. }
  409. }
  410. if ($bool_at_least_one_enabled) {
  411. $your_options['advisors'][0][$faculty_id] = $faculty_id;
  412. }
  413. } // foreach
  414. $dept_line = "";
  415. $params = array();
  416. $departments = fp_get_departments($user->school_id);
  417. $apt_from_depts = variable_get_for_school('appointments_from_departments', array(), intval($user->school_id));
  418. if (count($apt_from_depts) > 0) {
  419. $dept_line = "AND f.department_code IN (";
  420. foreach ($apt_from_depts as $dept_code) {
  421. $mdept_code = fp_get_machine_readable($dept_code);
  422. $dept_line .= ":dept_$mdept_code,";
  423. $params[":dept_$mdept_code"] = $dept_code;
  424. }
  425. $dept_line = rtrim($dept_line, ",");
  426. $dept_line .= ")";
  427. }
  428. // Any other staff at the university who have appointments allowed.
  429. if ($dept_line) {
  430. $res = db_query("SELECT * FROM users u, faculty f
  431. WHERE is_faculty = 1
  432. AND is_disabled = 0
  433. $dept_line
  434. ORDER BY l_name, f_name ", $params);
  435. while ($cur = db_fetch_array($res)) {
  436. $faculty_id = $cur['cwid'];
  437. $uid = intval($cur['uid']);
  438. $dept_code = $cur['department_code'];
  439. // Does this faculty member accept appointments?
  440. $event_types = content_get_content_for_faculty_id('schedule_event_type', $faculty_id);
  441. // fpm($event_types);
  442. $bool_at_least_one_enabled = FALSE;
  443. foreach ($event_types as $content) {
  444. if ($content->field__enabled['value'] == 'enabled') {
  445. $bool_at_least_one_enabled = TRUE;
  446. }
  447. }
  448. if ($bool_at_least_one_enabled && !in_array($faculty_id, $your_options['advisors'][0])) {
  449. $your_options['staff'][$dept_code][$faculty_id] = $faculty_id;
  450. }
  451. } // while
  452. } // if dept_line
  453. // TODO: Hook that modifies $your_options array, so that other modules can add or remove people
  454. // from it.
  455. // Display the various links
  456. $c = 0;
  457. foreach ($your_options as $type => $details) {
  458. foreach ($your_options[$type] as $dept_code => $details2) {
  459. foreach ($your_options[$type][$dept_code] as $faculty_id) {
  460. $faculty_name = fp_get_faculty_name($faculty_id);
  461. $faculty_user_id = db_get_user_id_from_cwid($faculty_id);
  462. $title = t("Your Advisor(s)");
  463. if ($type == 'staff') {
  464. $dept_title = @$departments[$dept_code];
  465. if (!$dept_title) $dept_title = $dept_code;
  466. $title = $dept_title . " - " . t("(Department/Unit)");
  467. if ($c == 0) {
  468. $rtn .= "<div class='schedule-appt-sep'>" . t("Other faculty/staff you may schedule appointments with:") . "</div>";
  469. }
  470. $c++;
  471. }
  472. $rtn .= "<h3>" . $title . "</h3>
  473. <ul>";
  474. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  475. $link = $base_url . '/' . fp_url("schedule-appointment/$faculty_user_id", '', FALSE);
  476. $rtn .= "<li>" . $faculty_name . " - <a href='$link'><i class='fa fa-calendar-plus-o'></i> Schedule appointment</a></li>";
  477. $rtn .= "</ul>";
  478. } // foreach
  479. }
  480. }
  481. } // user is student
  482. return $rtn;
  483. } // calendar_display_schedule_staff_page
  484. function calendar_perm() {
  485. $arr = array();
  486. $arr['can_accept_appointments'] = array(
  487. 'title' => t("Can accept appointments"),
  488. 'description' => t("Give to users who are allowed to accept appointments from students. For example, only give this permission to advisors, professors, etc."),
  489. );
  490. $arr['can_schedule_appointments_with_staff'] = array(
  491. 'title' => t("Can schedule appointments with faculty/staff"),
  492. 'description' => t("Give to users who are allowed to schedule appointments with faculty/staff. Ex: basically all students."),
  493. );
  494. $arr['can_cancel_own_appointments'] = array(
  495. 'title' => t("Can cancel own appointments"),
  496. 'description' => t("The user can cancel appointments which involve themselves. Give to both students and faculty/staff."),
  497. );
  498. $arr['administer_appointment_settings'] = array(
  499. 'title' => t("Administer appointment settings"),
  500. 'description' => t("The user can configure the global appointment settings within FlightPath. Only grant to trusted users."),
  501. );
  502. return $arr;
  503. }
  504. /**
  505. * The user has successfully completed their appointment scheduling. Display a Thank You page.
  506. */
  507. function calendar_display_schedule_appointment_completed_page($appointment_cid) {
  508. $rtn = "";
  509. $content = content_load($appointment_cid);
  510. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  511. $rtn .= "<p>" . t("Thank you for scheduling an appointment! Your details are visible below, and will appear
  512. in your FlightPath calendar as well.") . "</p>";
  513. $rtn .= "<p>" . t("You may close this window.") . "</p>";
  514. $rtn .= "<div class='appointment-summary-view'>";
  515. $rtn .= "<div><strong>" . t("Appointment Details:") . "</strong><br>" . $content->field__appointment_msg['display_value'] . "</div>";
  516. $rtn .= "</div>";
  517. return $rtn;
  518. } // calendar_display_schedule_appointment_completed_page
  519. /**
  520. * This page is where the user can configure their various appointment settings (like when they offer
  521. * them)
  522. */
  523. function calendar_display_user_appointment_settings_page() {
  524. global $user;
  525. $rtn = "";
  526. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  527. $day_names = array('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
  528. $base_url = $GLOBALS['fp_system_settings']['base_url'];
  529. $url = $base_url . '/' . fp_url("schedule-appointment/$user->id", '', false);
  530. fp_set_title($user->f_name . " " . $user->l_name . " (" . $user->cwid . ")");
  531. $rtn .= "<p>FlightPath allows you to offer appointment scheduling to students at this URL: <a href='$url' target='_blank'>$url</a> </p>
  532. <p>To begin, create a new Appointment Type you wish to offer
  533. (for example: Office Meeting, Video Call, etc), then specify what your availability is.</p>";
  534. $url = fp_url("content/add/schedule_event_type", "faculty_id=" . $user->cwid . "&window_mode=popup");
  535. $dtitle = t("Create New Appointment Type");
  536. $rtn .= "<a href='javascript:fpOpenLargeIframeDialog(\"$url\",\"$dtitle\");' class='button'><i class='fa fa-plus'></i> $dtitle</a>";
  537. $rtn .= "
  538. <table class='schedule-event-types'>
  539. <tr>
  540. <th class='actions'>Actions</th>
  541. <th>Enabled?</th>
  542. <th>Appointment Type Title</th>
  543. <th>Duration</th>
  544. <th>Buffer</th>
  545. <th>Video Meeting?</th>
  546. </tr>";
  547. // Display all of the event types for this faculty member.
  548. $event_types = content_get_content_for_faculty_id('schedule_event_type', $user->cwid);
  549. foreach ($event_types as $cid => $content) {
  550. $dtitle = t("Edit Event Type");
  551. $url = fp_url("content/$cid/edit", "window_mode=popup&content_tabs=false");
  552. $edit_link = "<a href='javascript:fpOpenLargeIframeDialog(\"$url\",\"$dtitle\");'><i class='fa fa-pencil'></i></a>";
  553. $video = "-";
  554. if ($content->field__video_meeting['value'] == 'zoom' && module_enabled('zoomapi')) {
  555. $video = "Zoom";
  556. }
  557. $rtn .= "<tr>
  558. <td class='actions'>$edit_link</td>
  559. <td>{$content->field__enabled['display_value']}</td>
  560. <td>$content->title</td>
  561. <td>{$content->field__event_duration_minutes['display_value']}</td>
  562. <td>{$content->field__event_buffer_minutes['display_value']}</td>
  563. <td>$video</td>
  564. </tr>";
  565. } // foreach
  566. $rtn .= "</table>";
  567. // Connect Zoom?
  568. if (module_enabled("zoomapi") && isset($GLOBALS['zoom_client_id'])) {
  569. $msg = t("To enable automatic Zoom meeting creation for video chats, you must first link your Zoom account with your FlightPath account by pressing
  570. the button below:");
  571. $button_text = t("Link Zoom Account");
  572. $link_class = "button";
  573. // is the user already set up with Zoom? If so, display a message, and change the install link to something like "Re-install Zoom", etc.
  574. $zoomapi_data = zoomapi_get_zoomapi_data_for_user($user->id);
  575. if ($zoomapi_data) {
  576. $msg = t("You have already linked your FlightPath account with Zoom! If you need to re-link your account, click the following:");
  577. $button_text = "Re-Link Zoom Account";
  578. $link_class = "";
  579. }
  580. $rtn .= "<p>" . t("<strong>Zoom:</strong> @msg", array("@msg" => $msg)) . "</p>";
  581. $url = zoomapi_get_zoom_install_url();
  582. $rtn .= "<div class='zoom-install-btn'>&nbsp; &nbsp; <a href='$url' class='$link_class' title='" . t($button_text) . "'><i class='fa fa-external-link'></i> " . t($button_text) . "</a> &nbsp;";
  583. if ($zoomapi_data) {
  584. $rtn .= t("To unlink/de-authorize Zoom from FlightPath, view the instructions here: ") . "<a href='https://flightpathacademics.com/help/61' target='_blank'>Uninstalling Zoom from FlightPath</a>";
  585. }
  586. $rtn .= "</div>";
  587. } // we have zoom client id set up.
  588. $rtn .= "<hr><h3>Unavailabile Times</h3>
  589. <p>Use the options below to define when you will be <em>unavailable</em> for appointments. For example, you should set that you are unavailable on
  590. weekends, mornings, and evenings outside of your work hours. You may create as many of these <em>Unavailable Time</em> records as you wish.</p>
  591. <p>To create specific one-off dates and times, you may enter appointments into your calendar.</p>
  592. <p>You may also provide a URL to an outside calendar (in ICS format) to mark your unavailable times.</p>";
  593. $url = fp_url("content/add/schedule_unavailable_time", "faculty_id=" . $user->cwid . "&window_mode=popup");
  594. $dtitle = t("Create New Unavailable Time");
  595. $rtn .= "<a href='javascript:fpOpenLargeIframeDialog(\"$url\",\"$dtitle\");' class='button'><i class='fa fa-plus'></i> Create New Unavailable Time</a>";
  596. // Display all of the schedule_unavailable_time nodes.
  597. $rtn .= "
  598. <table class='schedule-event-types schedule-unavailable-time'>
  599. <tr>
  600. <th class='actions'>Actions</th>
  601. <th>Short Name</th>
  602. <th>Days</th>
  603. <th>Unavailable Desc</th>
  604. <th>Start Time</th>
  605. <th>End Time</th>
  606. <th>External calendar?</th>
  607. </tr>";
  608. // Display all of the event types for this faculty member.
  609. $unavail_time = content_get_content_for_faculty_id('schedule_unavailable_time', $user->cwid);
  610. foreach($unavail_time as $cid => $content) {
  611. $tz = $content->field__timezone['value'];
  612. $ftz = friendly_timezone($tz);
  613. $start_time = date('g:ia', convert_time(strtotime("2020-01-01 " . $content->field__start_time['value']), 'UTC', $tz));
  614. $end_time = date('g:ia', convert_time(strtotime("2020-01-01 " . $content->field__end_time['value']), 'UTC', $tz));
  615. $days = "";
  616. if (is_array($content->field__days['value'])) {
  617. foreach ($content->field__days['value'] as $k => $v) {
  618. $days .= $day_names[$v] . ", ";
  619. }
  620. }
  621. $days = rtrim(trim($days), ","); // remove trailing comma
  622. $ics = '';
  623. if (trim($content->field__ics_url['value'])) {
  624. $ics = 'Y';
  625. $start_time = $end_time = "";
  626. }
  627. $time_selector_disp = $content->field__time_selector['display_value'];
  628. $time_selector = $content->field__time_selector['value'];
  629. if ($time_selector == "none") {
  630. $time_selector_disp = t("None on these days");
  631. $start_time = "";
  632. $end_time = "";
  633. }
  634. if ($time_selector == "" || $time_selector == "default") {
  635. $time_selector_disp = t("No appointments...");
  636. $start_time = "before " . $content->field__day_start_hour['display_value'];
  637. $end_time = "after " . $content->field__day_stop_hour['display_value'];
  638. }
  639. if ($time_selector == "manual") {
  640. $time_selector_disp = t("No appointments during...");
  641. }
  642. if ($ics) {
  643. $time_selector_disp = "Using external cal";
  644. $start_time = "";
  645. $end_time = "";
  646. }
  647. $dtitle = t("Edit Unavailability Time");
  648. $url = fp_url("content/$cid/edit", "window_mode=popup&content_tabs=false");
  649. $edit_link = "<a href='javascript:fpOpenLargeIframeDialog(\"$url\",\"$dtitle\");'><i class='fa fa-pencil'></i></a>";
  650. $rtn .= "<tr>
  651. <td class='actions'>$edit_link</td>
  652. <td>$content->title</td>
  653. <td>$days</td>
  654. <td>$time_selector_disp</td>
  655. <td>$start_time</td>
  656. <td>$end_time</td>
  657. <td>$ics</td>
  658. </tr>";
  659. } // foreach
  660. $rtn .= "</table>";
  661. return $rtn;
  662. } // calendar_display_user_appointment_settings_page
  663. /**
  664. * Implements hook_cron
  665. */
  666. function calendar_cron() {
  667. calendar_find_and_remind_notify_upcoming_appointments();
  668. } // hook_cron
  669. /**
  670. * This function will find appointments approaching within X number of minutes,
  671. * and send out notifications to all involved.
  672. *
  673. * We will keep track of ones we've already sent by looking at the notification_history table.
  674. */
  675. function calendar_find_and_remind_notify_upcoming_appointments() {
  676. $start_date = date('Y-m-d H:i:s', time());
  677. $end_date = date("Y-m-d H:i:s", strtotime("NOW + 30 MINUTES"));
  678. $res = db_query("SELECT DISTINCT(a.cid) FROM content__appointment a, content n
  679. WHERE (field__appointment_datetime BETWEEN ? AND ?)
  680. AND a.vid = n.vid
  681. AND a.cid = n.cid
  682. AND n.delete_flag = 0
  683. AND published = 1
  684. ORDER BY field__appointment_datetime ASC, a.vid DESC", array($start_date, $end_date));
  685. while ($cur = db_fetch_object($res)) {
  686. $cid = $cur->cid;
  687. $hid = db_result(db_query("SELECT hid FROM notification_history WHERE cid = ? AND notification_type='remind' LIMIT 1", array($cid)));
  688. if ($hid) {
  689. // We have already reminded the user(s) about this appointment. We can skip.
  690. continue;
  691. }
  692. // If we are here, then we need to remind the users about the appointment!
  693. $content = content_load($cid);
  694. $utc_appointment_datetime = strtotime($content->field__appointment_datetime['value']);
  695. $student_cwid = $content->field__student_id['value'];
  696. $faculty_cwid = $content->field__faculty_id['value'];
  697. $student_name = fp_get_student_name($student_cwid);
  698. $faculty_name = fp_get_faculty_name($faculty_cwid);
  699. // First, send a notification to the student.
  700. $student_user_id = db_get_user_id_from_cwid($student_cwid, 'student');
  701. $student_account = fp_load_user($student_user_id);
  702. $student_tz = fp_get_user_timezone($student_account);
  703. $apt_date = format_date(convert_time($utc_appointment_datetime, 'UTC', $student_tz), 'short');
  704. $msg = "APPOINTMENT REMINDER: You have an appointment soon with $faculty_name: $apt_date";
  705. $video_info = "";
  706. // If there is meeting data (eg, Zoom, include URLs and phone numbers here)
  707. if (strlen($content->field__video_data['value']) > 0) {
  708. $decoded = json_decode($content->field__video_data['value']);
  709. if ($decoded) {
  710. $video_info_test = @$decoded->video_info;
  711. if ($video_info_test) {
  712. $video_info = $video_info_test;
  713. }
  714. }
  715. $msg .= "\n" . $video_info;
  716. }
  717. notify_send_notification_to_user($student_user_id, $msg, $cid, 'appointment', 'remind');
  718. // Now, send message to faculty member
  719. $faculty_user_id = db_get_user_id_from_cwid($faculty_cwid, 'faculty');
  720. $faculty_account = fp_load_user($faculty_user_id);
  721. $faculty_tz = fp_get_user_timezone($faculty_account);
  722. $apt_date = format_date(convert_time($utc_appointment_datetime, 'UTC', $faculty_tz), 'short');
  723. $msg = "APPOINTMENT REMINDER: You have an appointment soon with $student_name: $apt_date";
  724. // If there is meeting data (eg, Zoom, include URLs and phone numbers here)
  725. if ($video_info) {
  726. $msg .= "\n" . $video_info;
  727. }
  728. notify_send_notification_to_user($faculty_user_id, $msg, $cid, 'appointment', 'remind');
  729. // No need to insert into the notification_history table, since these notification functions would have already done it.
  730. } // while
  731. } // calendar_find_and_notify_upcoming_appointments
  732. /**
  733. * Returns an array of upcoming appointments, where the user is specified by CWID. start_date and end_date
  734. * is meant to be in UTC, in the form of Y-m-d
  735. *
  736. * If start or end is left blank, we will use NOW and NOW + x DAYS respectively
  737. *
  738. * This function is used by the dashboard and similar situations.
  739. *
  740. */
  741. function calendar_get_upcoming_appointments_for_cwid($cwid, $start_date = "", $end_date = "") {
  742. $rtn = array();
  743. if ($start_date == "") {
  744. $start_date = date("Y-m-d H:i:s", strtotime("NOW"));
  745. }
  746. else {
  747. $start_date .= " 00:00:01";
  748. }
  749. if ($end_date == "") {
  750. $end_date = date("Y-m-d H:i:s", strtotime("NOW + 5 DAYS"));
  751. }
  752. else {
  753. $end_date .= " 23:59:59";
  754. }
  755. $res = db_query("SELECT DISTINCT(a.cid) FROM content__appointment a, content n
  756. WHERE (field__faculty_id = ? OR field__student_id = ?)
  757. AND (field__appointment_datetime BETWEEN ? AND ?)
  758. AND a.vid = n.vid
  759. AND a.cid = n.cid
  760. AND n.delete_flag = 0
  761. AND published = 1
  762. ORDER BY field__appointment_datetime ASC, a.vid DESC", array($cwid, $cwid, $start_date, $end_date));
  763. while ($cur = db_fetch_object($res)) {
  764. $cid = $cur->cid;
  765. $content = content_load($cid);
  766. $ap_datetime_ts = strtotime($content->field__appointment_datetime['value']);
  767. $year = date('Y', $ap_datetime_ts);
  768. $month = date('n', $ap_datetime_ts);
  769. $day = date('j', $ap_datetime_ts);
  770. $faculty_name = @fp_get_faculty_name($content->field__faculty_id['value']);
  771. $student_name = @fp_get_student_name($content->field__student_id['value'], TRUE);
  772. $rtn[] = array(
  773. 'utc_start_ts' => $ap_datetime_ts,
  774. 'utc_end_ts' => $ap_datetime_ts + (intval($content->field__appointment_duration_minutes['value']) * 60),
  775. 'cid' => $cid,
  776. 'content' => $content,
  777. 'faculty_name' => $faculty_name,
  778. 'student_name' => $student_name,
  779. );
  780. } // while
  781. return $rtn;
  782. }
  783. /**
  784. * Return back a list of appointment content nodes for this faculty member, which fall between the
  785. * specified datetimes.
  786. *
  787. * dates expected to look like: Y-m-d
  788. *
  789. */
  790. function calendar_get_appointments_for_faculty($faculty_id, $start_date, $end_date, $bool_include_external_cals = FALSE) {
  791. $rtn = array();
  792. $start_date .= " 00:00:01";
  793. $end_date .= " 23:59:59";
  794. $res = db_query("SELECT DISTINCT(a.cid) FROM content__appointment a, content n
  795. WHERE field__faculty_id = ?
  796. AND (field__appointment_datetime BETWEEN ? AND ?)
  797. AND a.vid = n.vid
  798. AND a.cid = n.cid
  799. AND n.delete_flag = 0
  800. AND published = 1
  801. ORDER BY a.vid DESC", array($faculty_id, $start_date, $end_date));
  802. while ($cur = db_fetch_object($res)) {
  803. $cid = $cur->cid;
  804. $content = content_load($cid);
  805. //$ap_datetime_ts = convert_time(strtotime($content->field__appointment_datetime['value']));
  806. $ap_datetime_ts = strtotime($content->field__appointment_datetime['value']);
  807. $year = date('Y', $ap_datetime_ts);
  808. $month = date('n', $ap_datetime_ts);
  809. $day = date('j', $ap_datetime_ts);
  810. $hm = date('g:ia', $ap_datetime_ts);
  811. $rtn[$year][$month][$day][$ap_datetime_ts] = array(
  812. 'start_ts' => $ap_datetime_ts,
  813. 'hm' => $hm,
  814. 'end_ts' => $ap_datetime_ts + (intval($content->field__appointment_duration_minutes['value']) * 60),
  815. 'origin' => 'fp',
  816. 'tz' => 'UTC',
  817. );
  818. } // while
  819. $faculty_user_id = db_get_user_id_from_cwid($faculty_id, 'faculty');
  820. $faculty_tz = fp_get_user_timezone($faculty_user_id);
  821. if ($bool_include_external_cals) {
  822. // Meaning, we should also look up the external ics feeds we may have.
  823. $x = fp_get_module_path('calendar', TRUE, FALSE) . '/lib/ics-parser/vendor/autoload.php';
  824. require_once($x);
  825. // Find external feeds (if any exist) for this user.
  826. $ics_feeds = array();
  827. $res = db_query("SELECT * FROM content__schedule_unavailable_time a, content n
  828. WHERE field__faculty_id = ?
  829. AND field__ics_url != ''
  830. AND a.vid = n.vid
  831. AND a.cid = n.cid
  832. AND n.delete_flag = 0
  833. AND published = 1
  834. ORDER BY a.vid DESC", array($faculty_id));
  835. while ($cur = db_fetch_array($res)) {
  836. $ics_url = trim($cur['field__ics_url']);
  837. if ($ics_url) {
  838. $ics_feeds[] = array(
  839. 'url' => $ics_url,
  840. 'tz' => $faculty_tz,
  841. 'cid' => $cur['cid'],
  842. );
  843. }
  844. }
  845. // DEV
  846. //$ics_feeds[] = array(
  847. // 'url' => "https://calendar.google.com/calendar/ical/example_url_here/public/basic.ics",
  848. // 'tz' => 'America/Chicago',
  849. //);
  850. foreach ($ics_feeds as $details) {
  851. $ics = $details['url'];
  852. $tz = @$details['tz'];
  853. $cid = $details['cid'];
  854. if (!$tz) $tz = $faculty_tz;
  855. $ical = NULL;
  856. $ical_str = "";
  857. try {
  858. // grab from cache or re-download the ics file?
  859. // See if more than 5 number of minutes have passed since we last pulled this file,
  860. // and regenerate it if so. Otherwise, use a database cache. Maybe serialized into the user_attributes table?
  861. $test = user_get_attribute($faculty_user_id, 'cache_ics_feed__' . $cid . '_reload_after_ts', 0);
  862. if (time() < $test) {
  863. // It has been cached previously, and the cache is still valid. Let's see if we can get what's there.
  864. $ical_str = user_get_attribute($faculty_user_id, 'cache_ics_feed__' . $cid, "");
  865. watchdog('calendar_ical', "Loading cached ics $ics ($cid)", array(), WATCHDOG_DEBUG);
  866. }
  867. if (!$ical_str || $ical_str == "") {
  868. watchdog('calendar_ical', "Downloading ics $ics ($cid) from web and caching", array(), WATCHDOG_DEBUG);
  869. $ical_str = fp_url_get_contents($ics);
  870. // Save to user_attributes as a cache for later retrieval
  871. user_set_attribute($faculty_user_id, 'cache_ics_feed__' . $cid, $ical_str);
  872. user_set_attribute($faculty_user_id, 'cache_ics_feed__' . $cid . '_reload_after_ts', time() + (5 * 60));
  873. }
  874. if ($ical_str == "") {
  875. watchdog('calendar_ical', "Error trying to read ical: $ics (cid: $cid). String is empty", array(), WATCHDOG_ERROR);
  876. fpm("error trying to read ical");
  877. return;
  878. }
  879. $ical = new ICal\ICal();
  880. $ical->initString($ical_str);
  881. if ($ical == NULL) {
  882. watchdog('calendar_ical', "Error trying to load ical: $ics string (cid: $cid) String: $ical_str", array(), WATCHDOG_ERROR);
  883. fpm("error trying to load ical_str");
  884. return;
  885. }
  886. $events = $ical->eventsFromRange($start_date, $end_date);
  887. foreach ($events as $event) {
  888. // if this is a "free" time, then the "transp" property will == "TRANSPARENT". If it's "busy" it will be "OPAQUE".
  889. // We want to ignore anything marked as "free".
  890. if (@$event->transp === "TRANSPARENT") continue;
  891. $utc_start = $event->dtstart_array[2]; // the UTC timestamp
  892. $utc_end = $event->dtend_array[2];
  893. // Convert into the OWNER's timezone!
  894. $converted_start = convert_time($utc_start, 'UTC', $tz);
  895. $converted_end = convert_time($utc_end, 'UTC', $tz);
  896. $ap_datetime_ts = $converted_start;
  897. $summary = $event->summary; // title. basically.
  898. $location = $event->location; // zoom link or whatever. Goes with body.
  899. $description = $event->description; // the body.
  900. // In order to catch events which span more than one day, let's add an entry
  901. // for each day covered between our start and end times.
  902. $last_date = '';
  903. for ($t = $converted_start; $t <= $converted_end; $t = $t + 21600) {
  904. $test_ts = $t;
  905. if (date('Y-m-d', $test_ts) == $last_date) continue; // same date already, skip it.
  906. $year = date('Y', $test_ts);
  907. $month = date('n', $test_ts);
  908. $day = date('j', $test_ts);
  909. $last_date = date('Y-m-d', $test_ts);
  910. $rtn[$year][$month][$day][$ap_datetime_ts] = array(
  911. 'start_ts' => $converted_start,
  912. 'end_ts' => $converted_end,
  913. 'converted_to_tz' => $tz,
  914. 'origin' => $ics,
  915. 'summary' => $summary,
  916. 'location' => $location,
  917. 'description' => filter_markup($description, 'basic'),
  918. );
  919. } // for t
  920. } // foreach events
  921. } catch (Exception $e) {
  922. watchdog('calendar_ical', "Couldn't load calendar ical $ics. Exception: <pre>" . print_r($e, TRUE) . "</pre>");
  923. fp_add_message($e->getMessage());
  924. }
  925. }
  926. } // bool_include_external_cals
  927. return $rtn;
  928. } // calendar_get_appointments_for_faculty
  929. /**
  930. * Returns back an array of time slots available for this faculty member and event_type
  931. *
  932. * We will convert all times to local timezone as well.
  933. *
  934. */
  935. function calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year) {
  936. $rtn = array();
  937. $event_type_content = content_load($event_type_cid);
  938. // Confirm that the faculty_id in this content matches our supplied faculty_id.
  939. if ($event_type_content->field__faculty_id['value'] != $faculty_id) {
  940. fp_add_message(t("Invalid schedule link."));
  941. fp_goto("<front>");
  942. return;
  943. }
  944. $prevent_less_than_hours = intval($event_type_content->field__prevent_less_than_hours['value']);
  945. // Get this faculty user's timezone.
  946. $faculty_user_id = db_get_user_id_from_cwid($faculty_id);
  947. $tz = fp_get_user_timezone($faculty_user_id);
  948. // Get all of our unavailable times into a single array, organized by days.
  949. $unavailable_times = content_get_content_for_faculty_id('schedule_unavailable_time', $faculty_id);
  950. $unavailable_by_days = array();
  951. foreach ($unavailable_times as $ut_content) {
  952. // If this is an ICS link, then skip for now.
  953. if (trim($ut_content->field__ics_url['value']) != '') continue;
  954. $time_selector = trim($ut_content->field__time_selector['value']);
  955. if ($time_selector == "") $time_selector = "default";
  956. foreach ($ut_content->field__days['value'] as $dow) {
  957. $start_end_array = array();
  958. if ($time_selector == "manual") {
  959. $st = $ut_content->field__start_time['value'];
  960. $et = $ut_content->field__end_time['value'];
  961. $start_end_array[] = array(
  962. 'start' => $st,
  963. 'end' => $et,
  964. 'convert_tz' => TRUE, // since these are in UTC, they will indeed need to be converted.
  965. );
  966. } // manual
  967. else if ($time_selector == "default") {
  968. // We will construct times based on the day_start_hour and stop_hour
  969. $st = $ut_content->field__day_start_hour['value'] . ":00:00";
  970. $et = $ut_content->field__day_stop_hour['value'] . ":00:00";
  971. if ($et == "0:00:00") $et = "23:59:59"; // meaning, we are stopping at midnight
  972. // We want to calculate out what our start time is LESS 1 second, so that we can begin accepting appointments
  973. // on the time we selected.
  974. $st_less_one_sec = $st; // default
  975. $start_hour = intval($ut_content->field__day_start_hour['value']);
  976. if ($start_hour > 0) {
  977. $st_less_one_sec = ($start_hour - 1) . ":59:59";
  978. }
  979. $et_plus_one_sec = $et; // default
  980. $stop_hour = intval($ut_content->field__day_stop_hour['value']);
  981. if ($stop_hour < 23) {
  982. $et_plus_one_sec = ($stop_hour + 1) . ":00:01";
  983. }
  984. // The first one is the start. We go from midnight to $st minus 1 second.
  985. $start_end_array[] = array(
  986. 'start' => "00:00:00",
  987. 'end' => $st_less_one_sec,
  988. '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.
  989. );
  990. // The second one is the end. We go from $et (plus one second) to 23:59:59
  991. $start_end_array[] = array(
  992. 'start' => $et_plus_one_sec,
  993. 'end' => "23:59:59",
  994. '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.
  995. );
  996. } // default
  997. else if ($time_selector == "none") {
  998. // The user doesn't work at all on the selected days.
  999. // We go from 00:00:00 to 23:59:59
  1000. $start_end_array[] = array(
  1001. 'start' => "00:00:00",
  1002. 'end' => "23:59:59",
  1003. '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.
  1004. );
  1005. } // none
  1006. // Now, populate our unavailable array
  1007. foreach ($start_end_array as $c => $details) {
  1008. $st = $details['start'];
  1009. $et = $details['end'];
  1010. $convert_tz = $details['convert_tz'];
  1011. // st and et are in UTC. The original faculty user's timezone is tz.
  1012. // Let's first convert them to the ORIGINAL timezone FROM UTC.
  1013. $sample_date = '2020-01-01';
  1014. // Grab the UTC times. Since we only care about hours/minutes, it's OK that we use the same date.
  1015. $utc_st = strtotime($sample_date . " " . $st);
  1016. $utc_et = strtotime($sample_date . " " . $et);
  1017. $tz_st = $utc_st;
  1018. $tz_et = $utc_et;
  1019. if ($convert_tz == TRUE) {
  1020. // Convert the UTC time into faculty member's timezone
  1021. $tz_st = convert_time($utc_st, 'UTC', $tz);
  1022. $tz_et = convert_time($utc_et, 'UTC', $tz);
  1023. }
  1024. // The start and end times are now in the original faculty member's timezone.
  1025. $unavailable_by_days[$dow][] = array(
  1026. 'start_time' => date("H:i", $tz_st),
  1027. 'end_time' => date("H:i", $tz_et),
  1028. 'tz' => $tz,
  1029. );
  1030. }
  1031. } // foreach field__days
  1032. } // foreach unavailable_times
  1033. // Add to the unavailable_by_days array:
  1034. // existing appointments on our FP calendar
  1035. $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);
  1036. // Okay, we need to find out how long this event lasts + any buffer time.
  1037. // 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
  1038. // we'd see a slot at: 11:30am, 11:50am, 12:10pm, 12:30pm, and so on.
  1039. $how_long_min = intval($event_type_content->field__event_duration_minutes['value']) + intval($event_type_content->field__event_buffer_minutes['value']);
  1040. $how_long_sec = $how_long_min * 60;
  1041. // Next, we are going to go through every single day of this month, to find out what all our slots are.
  1042. $total_days = cal_days_in_month(CAL_GREGORIAN, $month, $year);
  1043. for ($day = 1; $day <= $total_days; $day++) {
  1044. $begin_time = "00:00:00";
  1045. $end_time = "23:59:59";
  1046. $skip_begin_slot = array();
  1047. // We DO NOT NEED TO CONVERT for this!
  1048. $begin_time_ts = strtotime("$year-$month-$day $begin_time");
  1049. $end_time_ts = strtotime("$year-$month-$day $end_time");
  1050. $day_of_week = intval(date('w', $begin_time_ts));
  1051. // Loop from our begin time to our end time, by how_long_sec each step. We then
  1052. // want to see if there were any conflicts. If not, then it's safe to permit that time slot in our $rtn array
  1053. // for this day.
  1054. $begin_slot_ts = $begin_time_ts;
  1055. for ($end_slot_ts = $begin_time_ts + $how_long_sec; $end_slot_ts <= $end_time_ts; $end_slot_ts += $how_long_sec) {
  1056. // 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.
  1057. if ($begin_slot_ts < strtotime("NOW + $prevent_less_than_hours HOURS")) {
  1058. // get ready for next time through the loop
  1059. $begin_slot_ts = $end_slot_ts;
  1060. continue;
  1061. }
  1062. // Okay-- We have a slot of time-- from begin_slot_ts to end_slot_ts.
  1063. // check to see if there are any conflicts during this "slot".
  1064. $bool_conflict = FALSE;
  1065. // check our unavailable times
  1066. if (isset($unavailable_by_days[$day_of_week])) {
  1067. foreach ($unavailable_by_days[$day_of_week] as $details) {
  1068. $un_start_time = $details['start_time']; // These are now in their original timezone!
  1069. $un_end_time = $details['end_time']; // these are now in their original timezone!
  1070. $un_start_time_ts = strtotime("$year-$month-$day $un_start_time");
  1071. $un_end_time_ts = strtotime("$year-$month-$day $un_end_time");
  1072. // Now, does our "slot" conflict with this unavailable time window?
  1073. // Scenario #1: Our unavailable time completely engulfs our slot.
  1074. // Ex: { unavail [ slot ] }
  1075. if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $end_slot_ts) {
  1076. $bool_conflict = TRUE;
  1077. break;
  1078. }
  1079. // Scenario #2: Our slot completely engulfs our unavailable time window.
  1080. // Ex: [ slot { unavail } ]
  1081. if ($begin_slot_ts <= $un_start_time_ts && $end_slot_ts >= $un_end_time_ts) {
  1082. $bool_conflict = TRUE;
  1083. break;
  1084. }
  1085. // Scenario #3: Our unavail time starts before slot, but ends inside of the slot.
  1086. // Ex: { unavail [ slot } ]
  1087. if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $begin_slot_ts && $un_end_time_ts <= $end_slot_ts ) {
  1088. $bool_conflict = TRUE;
  1089. break;
  1090. }
  1091. // Scenario #4: Our unavail time starts after slot begins
  1092. // Ex: [ slot { unavail ] }
  1093. if ($un_start_time_ts >= $begin_slot_ts && $un_start_time_ts <= $end_slot_ts && $un_end_time_ts >= $end_slot_ts ) {
  1094. $bool_conflict = TRUE;
  1095. break;
  1096. }
  1097. } // foreach unavailable by days[dow]
  1098. }
  1099. // Okay, now check our existing_appointments, and see if there are any conflicts thee.
  1100. if (isset($existing_appointments[$year][$month][$day])) {
  1101. foreach ($existing_appointments[$year][$month][$day] as $appt) {
  1102. $un_start_time_ts = $appt['start_ts']; // These are in UTC
  1103. $un_end_time_ts = $appt['end_ts'];
  1104. // For reasons that aren't completely known to me, here we are going to add the time-converted
  1105. // begin_slot_ts to an array, which we'll use later to "prune" our return results.
  1106. // Now, does our "slot" conflict with this unavailable time window?
  1107. // Scenario #1: Our unavailable time completely engulfs our slot.
  1108. // Ex: { unavail [ slot ] }
  1109. if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $end_slot_ts) {
  1110. $bool_conflict = TRUE;
  1111. $skip_begin_slot[] = convert_time($begin_slot_ts, 'UTC', $tz);
  1112. break;
  1113. }
  1114. // Scenario #2: Our slot completely engulfs our unavailable time window.
  1115. // Ex: [ slot { unavail } ]
  1116. if ($begin_slot_ts <= $un_start_time_ts && $end_slot_ts >= $un_end_time_ts) {
  1117. $bool_conflict = TRUE;
  1118. $skip_begin_slot[] = convert_time($begin_slot_ts, 'UTC', $tz);
  1119. break;
  1120. }
  1121. // Scenario #3: Our unavail time stars before slot, but ends inside of the slot.
  1122. // Ex: { unavail [ slot } ]
  1123. if ($un_start_time_ts <= $begin_slot_ts && $un_end_time_ts >= $begin_slot_ts && $un_end_time_ts <= $end_slot_ts ) {
  1124. $bool_conflict = TRUE;
  1125. $skip_begin_slot[] = convert_time($begin_slot_ts, 'UTC', $tz);
  1126. break;
  1127. }
  1128. // Scenario #4: Our unavail time stars after slot begins
  1129. // Ex: [ slot { unavail ] }
  1130. if ($un_start_time_ts >= $begin_slot_ts && $un_start_time_ts <= $end_slot_ts && $un_end_time_ts >= $end_slot_ts ) {
  1131. $bool_conflict = TRUE;
  1132. $skip_begin_slot[] = convert_time($begin_slot_ts, 'UTC', $tz);
  1133. break;
  1134. }
  1135. } // foreach existing appt
  1136. } // if isset
  1137. ////////////
  1138. // If we made it here, then there are no conflicts.
  1139. // Let's add it in to our rtn array.
  1140. if ($bool_conflict == FALSE) {
  1141. $begin_hm = date('g:ia', $begin_slot_ts);
  1142. $rtn[$year][$month][$day][$begin_slot_ts] = array(
  1143. 'begin_slot_ts' => $begin_slot_ts,
  1144. 'end_slot_ts' => $end_slot_ts,
  1145. 'begin_hm' => $begin_hm,
  1146. 'end_hm' => date('g:ia', $end_slot_ts),
  1147. 'tz' => $tz,
  1148. 'how_long_min' => $how_long_min,
  1149. 'how_long_sec' => $how_long_sec,
  1150. );
  1151. }
  1152. // get ready for next time through the loop
  1153. $begin_slot_ts = $end_slot_ts;
  1154. } // for end_slot_ts
  1155. // Prune our rtn array based on the skip_begin_slot array. I know this is an inefficient hack. I am being
  1156. // slowly driven crazy by this timezone shit.
  1157. if (isset($rtn[$year][$month][$day])) {
  1158. foreach ($rtn[$year][$month][$day] as $bg => $deets) {
  1159. if (in_array($bg, $skip_begin_slot)) {
  1160. unset($rtn[$year][$month][$day][$bg]);
  1161. }
  1162. }
  1163. }
  1164. } // for day
  1165. return $rtn;
  1166. } // calendar_get_available_faculty_schedule
  1167. /**
  1168. * The confirmation form the user will see once they have made
  1169. * their schedule selections.
  1170. */
  1171. function calendar_schedule_appointment_confirm_form($faculty_user_id) {
  1172. global $user;
  1173. $form = array();
  1174. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  1175. $faculty_user = fp_load_user($faculty_user_id);
  1176. if (!$faculty_user || $faculty_user->is_faculty != TRUE) {
  1177. return "<p>" . t("We're sorry! The link you followed is either invalid, or the selected user ID is not listed as a faculty/staff member at your
  1178. insititution.") . "</p>";
  1179. }
  1180. $faculty_id = $faculty_user->cwid;
  1181. $faculty_name = fp_get_faculty_name($faculty_id);
  1182. $faculty_tz = fp_get_user_timezone($faculty_user);
  1183. fp_set_title(t("Confirm Appointment with @name", array("@name" => $faculty_name)));
  1184. $event_type_cid = intval($_REQUEST['event_type_cid']);
  1185. $content = content_load($event_type_cid);
  1186. $date = $_REQUEST['date'];
  1187. $begin_hm = $_REQUEST['begin_hm'];
  1188. $temp = explode('-', $date);
  1189. $year = intval($temp[0]);
  1190. $month = intval($temp[1]);
  1191. $day = intval($temp[2]);
  1192. $month_year = $month . '_' . $year;
  1193. $student_tz = fp_get_user_timezone();
  1194. $form['student_tz'] = array(
  1195. 'type' => 'hidden',
  1196. 'value' => $student_tz,
  1197. );
  1198. $form['faculty_tz'] = array(
  1199. 'type' => 'hidden',
  1200. 'value' => $faculty_tz,
  1201. );
  1202. $url = fp_url("schedule-appointment/$faculty_user_id", "stage=3&event_type_cid=$content->cid&day=$day&month_year=$month_year");
  1203. $link_back = "<a href='$url' title='Back'><i class='fa fa-arrow-circle-left'></i></a>&nbsp; &nbsp;";
  1204. $form['mark_top'] = array(
  1205. 'value' => "<h3>$link_back" . t("Please confirm to schedule your appointment:") . "</h3>",
  1206. );
  1207. $schedule_time_ts = strtotime($date . " " . $begin_hm); // currently this is in the STUDENT's TIMEZONE. We want to convert it to UTC
  1208. $offset = get_timezone_offset('UTC', $student_tz);
  1209. $utc_schedule_time_ts = $schedule_time_ts - $offset;
  1210. $form['utc_schedule_time_ts'] = array(
  1211. 'type' => 'hidden',
  1212. 'value' => $utc_schedule_time_ts,
  1213. );
  1214. $formatted_date_for_student = format_date($schedule_time_ts, "pretty");;
  1215. $student_timezone_pretty = friendly_timezone($student_tz);
  1216. if ($student_timezone_pretty) $student_timezone_pretty = "(" . $student_timezone_pretty . ")";
  1217. // display all our information to the user to confirm
  1218. $html = "";
  1219. $html .= "<div class='confirm-schedule-appt-type'><strong>Appointment Type:</strong> $content->title</div>";
  1220. $html .= "<div class='confirm-schedule-appt-datetime'><strong>Date & Time:</strong> $formatted_date_for_student $student_timezone_pretty</div>";
  1221. $form['formatted_date_for_student'] = array(
  1222. 'type' => 'hidden',
  1223. 'value' => $formatted_date_for_student,
  1224. );
  1225. $form['schedule_date'] = array(
  1226. 'type' => 'hidden',
  1227. 'value' => $date,
  1228. );
  1229. $form['schedule_begin_hm'] = array(
  1230. 'type' => 'hidden',
  1231. 'value' => $begin_hm,
  1232. );
  1233. $form['faculty_user_id'] = array(
  1234. 'type' => 'hidden',
  1235. 'value' => $faculty_user_id,
  1236. );
  1237. $form['event_type_cid'] = array(
  1238. 'type' => 'hidden',
  1239. 'value' => $event_type_cid,
  1240. );
  1241. $form['mark_html'] = array(
  1242. 'value' => $html,
  1243. );
  1244. $name = $cwid = $email = $phone = "";
  1245. $field_attributes = array();
  1246. if ($user->id > 0) {
  1247. // Meaning, the user is logged in!
  1248. $name = $user->f_name . " " . $user->l_name;
  1249. $cwid = $user->cwid;
  1250. $email = $user->email;
  1251. $phone = engagements_convert_to_pretty_phone_number(@$user->attributes['mobile_phone']);
  1252. $field_attributes = array("readonly" => "readonly", "class" => "readonly");
  1253. }
  1254. $form['name'] = array(
  1255. 'type' => 'textfield',
  1256. 'label' => t('Your Name:'),
  1257. 'value' => $name,
  1258. 'attributes' => $field_attributes,
  1259. );
  1260. $form['cwid'] = array(
  1261. 'type' => 'textfield',
  1262. 'label' => t('Your CWID:'),
  1263. 'value' => $cwid,
  1264. 'attributes' => $field_attributes,
  1265. );
  1266. $form['email'] = array(
  1267. 'type' => 'textfield',
  1268. 'label' => t('Your Email:'),
  1269. 'value' => $email,
  1270. 'attributes' => $field_attributes,
  1271. );
  1272. $form['phone'] = array(
  1273. 'type' => 'textfield',
  1274. 'label' => t('Your Phone:'),
  1275. 'value' => $phone,
  1276. 'attributes' => $field_attributes,
  1277. );
  1278. $form['comments'] = array(
  1279. 'type' => 'textarea',
  1280. 'label' => t('Additional Comments:'),
  1281. 'description' => t("(Optional) Please enter any comments or details you'd like me to know before our meeting."),
  1282. );
  1283. $form['submit_btn'] = array(
  1284. 'type' => 'submit',
  1285. 'spinner' => TRUE,
  1286. 'value' => t('Confirm Appointment & Save!'),
  1287. );
  1288. // Set our breadcrumbs
  1289. $crumbs = array();
  1290. $crumbs[] = array(
  1291. 'text' => t('Schedule Appointment with @name', array("@name" => $faculty_name)),
  1292. 'path' => "schedule-appointment/$faculty_user_id",
  1293. );
  1294. $crumbs[] = array(
  1295. 'text' => $content->title,
  1296. 'path' => "schedule-appointment/$faculty_user_id",
  1297. 'query' => "stage=2&event_type_cid=$content->cid",
  1298. );
  1299. $crumbs[] = array(
  1300. 'text' => t('Select Time'),
  1301. 'path' => "schedule-appointment/$faculty_user_id",
  1302. 'query' => "stage=3&event_type_cid=$content->cid&day=$day&month_year=$month_year",
  1303. );
  1304. fp_set_breadcrumbs($crumbs);
  1305. return $form;
  1306. } // calendar_schedule_appointment_confirm_form
  1307. /**
  1308. * This is our last chance to validate the form before saving.
  1309. *
  1310. * - Make sure the CWID goes to a real student
  1311. * - Make sure the time slot is still available! It might have gotten booked while we've been
  1312. * doing this.
  1313. */
  1314. function calendar_schedule_appointment_confirm_form_validate($form, &$form_state) {
  1315. $faculty_user_id = intval($form_state['values']['faculty_user_id']);
  1316. $faculty_user = fp_load_user($faculty_user_id);
  1317. $faculty_cwid = $faculty_user->cwid;
  1318. $faculty_name = $faculty_user->f_name . " " . $faculty_user->l_name;
  1319. $schedule_date = $form_state['values']['schedule_date'];
  1320. $schedule_begin_hm = $form_state['values']['schedule_begin_hm'];
  1321. $schedule_time_ts = strtotime($schedule_date . " " . $schedule_begin_hm);
  1322. $utc_schedule_time_ts = $form_state['values']['utc_schedule_time_ts'];
  1323. $month = date('n', $schedule_time_ts);
  1324. $year = date('Y', $schedule_time_ts);
  1325. $day = date('j', $schedule_time_ts);
  1326. $event_type_cid = intval($form_state['values']['event_type_cid']);
  1327. $event_type_content = content_load($event_type_cid);
  1328. $event_duration_minutes = intval($event_type_content->field__event_duration_minutes['value']);
  1329. // Confirm that the faculty_cwid matches this event type's faculty member.
  1330. if ($faculty_cwid != $event_type_content->field__faculty_id['value']) {
  1331. form_error('', t("Sorry, but there was an issue attempting to save this appointment. Please try again."));
  1332. return;
  1333. }
  1334. $db = get_global_database_handler();
  1335. $student_name = trim($form_state['values']['name']);
  1336. $student_cwid = trim($form_state['values']['cwid']);
  1337. // Make sure this cwid actually exists.
  1338. $x = $db->get_student_name($student_cwid);
  1339. if (!$x) {
  1340. $x = $db->get_faculty_name($student_cwid);
  1341. if (!$x) {
  1342. form_error('cwid', t("Sorry, but the CWID entered does not match any records. Please make sure you typed it correctly and try again."));
  1343. return;
  1344. }
  1345. }
  1346. $student_email = trim($form_state['values']['email']);
  1347. $student_phone = trim($form_state['values']['phone']);
  1348. $student_comments = trim($form_state['values']['comments']);
  1349. $bool_found_it = FALSE;
  1350. $available_slots = calendar_get_available_faculty_schedule($faculty_cwid, $event_type_cid, $month, $year);
  1351. $avail_times = calendar_get_available_times_on_date($available_slots, $faculty_user_id, $year, $month, $day, $event_type_cid);
  1352. foreach ($avail_times as $disp_hm => $details) {
  1353. if ($disp_hm == $schedule_begin_hm){
  1354. $bool_found_it = TRUE;
  1355. break;
  1356. }
  1357. }
  1358. // make sure this event time is still available!
  1359. // NOTE: Comment out for dev, but remove comments for production! Otherwise users can hit "back" and save again to enter more than one meeting at the same time.
  1360. $available_slots = calendar_get_available_faculty_schedule($faculty_cwid, $event_type_cid, $month, $year);
  1361. if (!isset($available_slots[$year][$month][$day][$schedule_time_ts])) {
  1362. form_error('', t("Sorry, but the time you selected for your appointment is no longer available. Please select a new time."));
  1363. return;
  1364. }
  1365. if (!$bool_found_it) {
  1366. form_error('', t("Sorry, but the time you selected for your appointment is no longer available. Please select a new time."));
  1367. return;
  1368. }
  1369. ////////////////////////////////////
  1370. // We have made it this far with no errors. If this is a video meeting, let's attempt to create that meeting.
  1371. // If this is a Zoom meeting, then attempt to create the zoom meeting and save it in form_state['values']['field__video_data'] so it
  1372. // gets saved with the content.
  1373. if (module_enabled("zoomapi") && @$event_type_content->field__video_meeting['value'] == 'zoom') {
  1374. //gmt_date_time must look like yyyy-MM-ddTHH:mm:ssZ. For example: 2020-03-31T22:02:00Z. The Z tells it that we are using GMT/UTC time.
  1375. $gmt_date_time = date("Y-m-d\TH:i:s\Z", $utc_schedule_time_ts);
  1376. $response = zoomapi_create_zoom_meeting($faculty_user_id, $gmt_date_time, $event_duration_minutes, "Video meeting - $faculty_name / $student_name");
  1377. if (!$response) {
  1378. form_error("", t("Sorry, but there was an issue creating the Zoom meeting for this appointment. Your appointment has not been saved.
  1379. Please try another method, or contact the IT administrator to let them know of this issue."));
  1380. return;
  1381. }
  1382. $form_state['values']['video_data'] = $response;
  1383. $form_state['values']['video_type'] = "zoom";
  1384. $form_state['values']['video_account_owner_user_id'] = $faculty_user_id;
  1385. }
  1386. //form_error("title", 'hey');
  1387. //return;
  1388. // Okay, if we made it here, then we are good to proceed and save!
  1389. } // hook_validate
  1390. /**
  1391. * We passed validation, it's time to actually submit now!
  1392. */
  1393. function calendar_schedule_appointment_confirm_form_submit($form, &$form_state) {
  1394. $schedule_date = $form_state['values']['schedule_date'];
  1395. $schedule_begin_hm = $form_state['values']['schedule_begin_hm'];
  1396. $schedule_time_ts = strtotime($schedule_date . " " . $schedule_begin_hm);
  1397. $utc_schedule_time_ts = $form_state['values']['utc_schedule_time_ts'];
  1398. $formatted_date_for_student = $form_state['values']['formatted_date_for_student'];
  1399. $fstudent_tz = friendly_timezone($form_state['values']['student_tz']);
  1400. $faculty_tz = $form_state['values']['faculty_tz'];
  1401. $ffaculty_tz = friendly_timezone($faculty_tz);
  1402. $faculty_user_id = intval($form_state['values']['faculty_user_id']);
  1403. $faculty_user = fp_load_user($faculty_user_id);
  1404. $faculty_cwid = $faculty_user->cwid;
  1405. $faculty_name = fp_get_faculty_name($faculty_cwid);
  1406. $event_type_cid = intval($form_state['values']['event_type_cid']);
  1407. $event_type_content = content_load($event_type_cid);
  1408. $event_additional_msg = trim(@$event_type_content->field__additional_email_msg['display_value']);
  1409. $event_duration_minutes = intval($event_type_content->field__event_duration_minutes['value']);
  1410. $student_name = trim(filter_markup($form_state['values']['name'], 'plain'));
  1411. $student_cwid = trim(filter_markup($form_state['values']['cwid'], 'plain'));
  1412. $student_email = trim(filter_markup($form_state['values']['email'], 'plain'));
  1413. $student_phone = trim(filter_markup($form_state['values']['phone'], 'plain'));
  1414. $student_comments = trim(filter_markup($form_state['values']['comments'], 'plain'));
  1415. $video_info = $video_data = $join_url = $dial_in = $meeting_id = "";
  1416. $location = t("In-Person");
  1417. $event_title = "Meeting between $student_name and $faculty_name";
  1418. if (isset($form_state['values']['video_data'])) {
  1419. $video_data = $form_state['values']['video_data'];
  1420. $decoded = json_decode($video_data);
  1421. if ($form_state['values']['video_type'] == 'zoom') {
  1422. $meeting_id = $decoded->id;
  1423. $join_url = $decoded->join_url;
  1424. $location = $join_url;
  1425. $dial_in = $decoded->settings->global_dial_in_numbers[0]->number;
  1426. if (function_exists("engagements_convert_to_pretty_phone_number")) {
  1427. $dial_in = engagements_convert_to_pretty_phone_number($dial_in, TRUE);
  1428. }
  1429. $event_title = t("Zoom meeting between @stu and @fac", array("@stu" => $student_name, "@fac" => $faculty_name));
  1430. $video_info .= "<p><b>" . t("Zoom Meeting:") . "</b> <a href='$join_url'>" . $join_url . "</a> \n<br> " . t("Be sure to download the Zoom app on your phone, tablet, or computer.") . " \n<br> " . t("To dial in instead of using video chat, you may dial:<br>\n @dial (ID: @id)", array("@dial" => $dial_in, "@id" => $meeting_id)) . ".</p>\n";
  1431. $decoded->video_info = $video_info; // add to our json object for ease of use later.
  1432. $decoded->video_provider = "zoom"; // declare that this is a "zoom" meeting.
  1433. $decoded->video_account_owner_user_id = $form_state['values']['video_account_owner_user_id'];
  1434. $video_data = json_encode($decoded); // re-encode as a simpl string, so we can add to the database.
  1435. }
  1436. }
  1437. // NOTE: the "\n" at the end is important, since when we txt these the tags get stripped.
  1438. $msg = "";
  1439. if ($event_additional_msg) {
  1440. $msg .= "<p>" . $event_additional_msg . "</p>\n-----\n";
  1441. }
  1442. $msg .= "<p><b>" . t("Scheduled:") . "</b> $event_type_content->title " . t("between") . " $student_name " . t("and") . " $faculty_name</p>\n";
  1443. $msg .= "<p><b>" . t("Date and Time:") . "</b> " . format_date(convert_time($utc_schedule_time_ts), 'pretty') . " ($fstudent_tz)</p>\n";
  1444. $msg .= "<p><b>" . t("Duration:") . "</b> " . $event_duration_minutes . " min</p>\n";
  1445. $msg .= $video_info;
  1446. if ($student_comments) {
  1447. $msg .= "<p><b>" . t("Student comments:") . "</b> $student_comments</p>\n";
  1448. }
  1449. // We will be creating a new appointment content node programmatically, then calling content_save() on it.
  1450. // We will then redirect to a confirmation page.
  1451. // This is a NEW content obj!
  1452. $content = new stdClass();
  1453. $content->type = 'appointment';
  1454. $content->cid = "new";
  1455. $content->published = 1;
  1456. $content->title = "appointment content saved on " . format_date(convert_time(time()));
  1457. $content->field__appointment_datetime['value'] = date('Y-m-d H:i:s', $utc_schedule_time_ts);
  1458. $content->field__appointment_duration_minutes['value'] = $event_duration_minutes;
  1459. $content->field__faculty_id['value'] = $faculty_cwid;
  1460. $content->field__student_id['value'] = $student_cwid;
  1461. $content->field__appointment_type['value'] = 'advising';
  1462. $content->field__appointment_msg['value'] = $msg;
  1463. $content->field__video_data['value'] = $video_data;
  1464. content_save($content);
  1465. $acid = $content->cid; // save for later.
  1466. // save as an "activity record" for this student.
  1467. $acontent = new stdClass();
  1468. $acontent->type = 'activity_record';
  1469. $acontent->cid = "new";
  1470. $acontent->published = 1;
  1471. $acontent->delete_flag = 0;
  1472. $acontent->title = t('Student has scheduled an appointment with @fn.', array("@fn" => $faculty_name));
  1473. $acontent->field__student_id['value'] = $student_cwid;
  1474. $content->field__faculty_id['value'] = $faculty_cwid;
  1475. $acontent->field__activity_type['value'] = 'calendar';
  1476. content_save($acontent);
  1477. // Get ics invitation string for email attachments
  1478. $start_dt = date("Ymd\THis\Z", $utc_schedule_time_ts);
  1479. $end_dt = date("Ymd\THis\Z", $utc_schedule_time_ts + ($event_duration_minutes * 60));
  1480. $ics = calendar_get_ics_invitation_string($event_title, $location, $video_info, $start_dt, $end_dt);
  1481. $attachment = array(
  1482. 'invite.ics' => $ics,
  1483. );
  1484. // Send notifications
  1485. // First, send a notification to the student.
  1486. $student_user_id = db_get_user_id_from_cwid($student_cwid, 'student');
  1487. $nmsg = "You have scheduled an appointment with $faculty_name:\n";
  1488. notify_send_notification_to_user($student_user_id, $nmsg . $msg, $acid, 'appointment', '', $attachment);
  1489. // Now, send a notification to the faculty member.
  1490. $nmsg = "$student_name has scheduled an appointment with you:\n";
  1491. $msg = "";
  1492. if ($event_additional_msg) {
  1493. $msg .= "<p>" . $event_additional_msg . "</p>\n-----\n";
  1494. }
  1495. $msg .= "<p><b>" . t("Scheduled:") . "</b> $event_type_content->title between $student_name and $faculty_name</p>\n";
  1496. $msg .= "<p><b>" . t("Date and Time:") . "</b> " . format_date(convert_time($utc_schedule_time_ts, 'UTC', $faculty_tz), 'pretty') . " ($ffaculty_tz)</p>\n";
  1497. $msg .= "<p><b>" . t("Duration:") . "</b> " . $event_duration_minutes . " min</p>\n";
  1498. $msg .= $video_info;
  1499. notify_send_notification_to_user($faculty_user_id, $nmsg . $msg, $acid, 'appointment', '', $attachment);
  1500. watchdog("calendar", "schedule_appointment student_id:$student_cwid, faculty_id:$faculty_cwid, appointment details: " . ppm($content, TRUE), array());
  1501. fp_goto("schedule-appointment-completed/$content->cid");
  1502. } // ... submit
  1503. /**
  1504. * This is the page which lets students schedule an appointment with the faculty member
  1505. * supplied in the user_id.
  1506. *
  1507. * Notice this is USER_ID and not CWID! This is so that the cwid is kept secret, since it might
  1508. * be private information.
  1509. */
  1510. function calendar_display_schedule_appointment_page($faculty_user_id) {
  1511. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  1512. $faculty_user = fp_load_user($faculty_user_id);
  1513. if (!$faculty_user || $faculty_user->is_faculty != TRUE) {
  1514. return "<p>" . t("We're sorry! The link you followed is either invalid, or the selected user ID is not listed as a faculty/staff member at your
  1515. insititution.") . "</p>";
  1516. }
  1517. $rtn = "";
  1518. $faculty_id = $faculty_user->cwid;
  1519. $faculty_name = fp_get_faculty_name($faculty_id);
  1520. fp_set_title(t("Schedule Appointment with @name", array("@name" => $faculty_name)));
  1521. $image_url = @$faculty_user->settings['image_url'];
  1522. $image_html = "";
  1523. if ($image_url) {
  1524. $image_html .= "<span class='small-circle-profile-image'>
  1525. <img src='$image_url'>
  1526. </span>";
  1527. }
  1528. $stage = trim($_REQUEST['stage']);
  1529. if ($stage == '' || $stage == '1') {
  1530. $event_types = content_get_content_for_faculty_id('schedule_event_type', $faculty_id);
  1531. $rtn .= $image_html;
  1532. $rtn .= "<p class='select-directions'>" . t("Begin by selecting which type of appointment you wish to schedule.") . "</p>
  1533. <div class='select-from-event-types'>";
  1534. foreach($event_types as $cid => $content) {
  1535. if ($content->field__enabled['value'] != 'enabled') continue;
  1536. $title = $content->title;
  1537. $description = trim($content->field__description['display_value']);
  1538. $url = fp_url("schedule-appointment/$faculty_user_id", "stage=2&event_type_cid=$cid");
  1539. $rtn .= "<a class='event-type' href='$url'>
  1540. <div class='event-type-title'>$title <i class='fa fa-arrow-circle-right'></i></div>
  1541. <div class='event-type-description'>$description</div>
  1542. </a>";
  1543. }
  1544. $rtn .= '</div>';
  1545. //$rtn .= "<br><br><div class='tzexplain'><i class='tzglobe fa fa-clock-o'></i> " . t("The time you select will be shown in your timezone of %tz.", array("%tz" => fp_get_user_timezone())) . "</div>";
  1546. } // stage 1
  1547. // Having selected the month, get a list of *unavailble* dates and times for this faculty member, for the selected month.
  1548. if ($stage == '2') {
  1549. $event_type_cid = intval($_REQUEST['event_type_cid']);
  1550. $content = content_load($event_type_cid);
  1551. $month_year = trim(@$_REQUEST['month_year']);
  1552. if (!$month_year) {
  1553. // Not set, so use today's month and year.
  1554. $month = date('n');
  1555. $year = date('Y');
  1556. $month_year = $month . "_" . $year;
  1557. }
  1558. $temp = explode("_", $month_year);
  1559. $month = intval($temp[0]);
  1560. $year = intval($temp[1]);
  1561. $url = fp_url("schedule-appointment/$faculty_user_id");
  1562. $link_back = "<a href='$url' title='Back'><i class='fa fa-arrow-circle-left'></i></a>&nbsp; &nbsp;";
  1563. $rtn .= "<h3>$link_back$faculty_name has time available on the following days:</h3>";
  1564. $title = $content->title;
  1565. $description = trim($content->field__description['display_value']);
  1566. $rtn .= "<div class='event-type-box'>
  1567. <div class='event-type-title'>$title</div>
  1568. <div class='event-type-description'>$description</div>
  1569. </div>";
  1570. // Get array of unavailable dates/times for the faculty user, for the selected month and year.
  1571. $available_slots = calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year);
  1572. // Draw a mini calendar, and make clickable any days which are at least partially available.
  1573. $rtn .= calendar_render_mini_calendar($month, $year, $faculty_user_id, $event_type_cid, $available_slots);
  1574. $crumbs = array();
  1575. $crumbs[] = array(
  1576. 'text' => t('Schedule Appointment with @name', array("@name" => $faculty_name)),
  1577. 'path' => "schedule-appointment/$faculty_user_id",
  1578. );
  1579. fp_set_breadcrumbs($crumbs);
  1580. } // stage = 2
  1581. // The user has selected a day, display the available times.
  1582. if ($stage == '3') {
  1583. $event_type_cid = intval($_REQUEST['event_type_cid']);
  1584. $content = content_load($event_type_cid);
  1585. $month_year = trim(@$_REQUEST['month_year']);
  1586. if (!$month_year) {
  1587. // Not set, so use today's month and year.
  1588. $month = date('n');
  1589. $year = date('Y');
  1590. $month_year = $month . "_" . $year;
  1591. }
  1592. $temp = explode("_", $month_year);
  1593. $month = intval($temp[0]);
  1594. $year = intval($temp[1]);
  1595. $day = intval($_REQUEST['day']);
  1596. $url = fp_url("schedule-appointment/$faculty_user_id", "stage=2&event_type_cid=$event_type_cid");
  1597. $link_back = "<a href='$url' title='Back'><i class='fa fa-arrow-circle-left'></i></a>&nbsp; &nbsp;";
  1598. $rtn .= "<h3>$link_back" . t("Select a time for") . " " . format_date(strtotime("$year-$month-$day"), '', 'l, F jS Y') . "</h3>";
  1599. $title = $content->title;
  1600. $description = trim($content->field__description['display_value']);
  1601. $rtn .= "<div class='event-type-box'>
  1602. <div class='event-type-title'>$title</div>
  1603. <div class='event-type-description'>$description</div>
  1604. </div>";
  1605. $rtn .= "<div class='event-select-time'>";
  1606. $rtn .= "<div class='tzexplain'><i class='tzglobe fa fa-globe'></i> " . t("Times are shown in your timezone of <br>%tz", array("%tz" => friendly_timezone(fp_get_user_timezone()))) . "</div>";
  1607. // Get array of unavailable dates/times for the faculty user, for the selected month and year.
  1608. $available_slots = calendar_get_available_faculty_schedule($faculty_id, $event_type_cid, $month, $year);
  1609. $avail_times = calendar_get_available_times_on_date($available_slots, $faculty_user_id, $year, $month, $day, $event_type_cid);
  1610. foreach ($avail_times as $disp_hm => $details) {
  1611. $rtn .= $details['html'];
  1612. }
  1613. $rtn .= "</div>";
  1614. $crumbs = array();
  1615. $crumbs[] = array(
  1616. 'text' => t('Schedule Appointment with @name', array("@name" => $faculty_name)),
  1617. 'path' => "schedule-appointment/$faculty_user_id",
  1618. );
  1619. $crumbs[] = array(
  1620. 'text' => $content->title,
  1621. 'path' => "schedule-appointment/$faculty_user_id",
  1622. 'query' => "stage=2&event_type_cid=$content->cid",
  1623. );
  1624. fp_set_breadcrumbs($crumbs);
  1625. } // stage 3
  1626. return $rtn;
  1627. } // calendar_display_schedule_appointment_page
  1628. function calendar_get_available_times_on_date($available_slots, $faculty_user_id, $year, $month, $day, $event_type_cid = 0) {
  1629. $rtn = array();
  1630. foreach ($available_slots[$year][$month][$day] as $slot) {
  1631. $begin_hm = $slot['begin_hm'];
  1632. $tz = $slot['tz'];
  1633. $student_timezone = fp_get_user_timezone();
  1634. // Okay, here is where we actually convert FROM the faculty timezone into the STUDENT's timezone!
  1635. $offset = get_timezone_offset($student_timezone, $tz);
  1636. $test_time = strtotime("$year-$month-$day $begin_hm");
  1637. $adjusted_student_tz_time = $test_time - $offset;
  1638. $student_begin_hm = date("g:ia", $adjusted_student_tz_time);
  1639. $url = fp_url("schedule-appointment/$faculty_user_id/confirm", "event_type_cid=$event_type_cid&date=$year-$month-$day&begin_hm=$student_begin_hm");
  1640. //$rtn .= "<a href='$url' class='select-time'>
  1641. // $student_begin_hm
  1642. // </a>";
  1643. $rtn[$student_begin_hm]['html'] = "
  1644. <a href='$url' class='select-time'>
  1645. $student_begin_hm
  1646. </a> ";
  1647. } // foreach
  1648. return $rtn;
  1649. }
  1650. // From: https://stackoverflow.com/questions/10309094/display-calendar-on-php/18752260
  1651. function calendar_render_mini_calendar($month, $year, $faculty_user_id, $event_type_cid = 0, $available_slots = array()) {
  1652. $today_num = intval(date('j', convert_time(time())));
  1653. $today_month = intval(date('n', convert_time(time())));
  1654. $today_year = intval(date('Y', convert_time(time())));
  1655. $day_name_length = 3;
  1656. $first_day = 0;
  1657. $month_ts = convert_time(strtotime("$year-$month-01 07:01:01")); // get us into the correct month.
  1658. $month_title = date('M', $month_ts) . " $year";
  1659. $month_year = $month . "_$year";
  1660. $first_of_month = mktime(0, 0, 0, $month, 1, $year);
  1661. // remember that mktime will automatically correct if invalid dates are entered
  1662. // for instance, mktime(0,0,0,12,32,1997) will be the date for Jan 1, 1998
  1663. // this provides a built in "rounding" feature
  1664. $day_names = array('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat');
  1665. list($month, $year, $month_name, $weekday) = explode(',', strftime('%m, %Y, %B, %w', $first_of_month));
  1666. $weekday = ($weekday + 7 - $first_day) % 7; //adjust for $first_day
  1667. // Make sure they are integers.
  1668. $month = intval($month);
  1669. $year = intval($year);
  1670. $prev_month = $month - 1;
  1671. $prev_year = $year;
  1672. if ($prev_month < 1) {
  1673. $prev_month = 12;
  1674. $prev_year--;
  1675. }
  1676. $prev_url = fp_url("schedule-appointment/$faculty_user_id", "stage=2&event_type_cid=$event_type_cid&month_year=$prev_month" . "_" . "$prev_year");
  1677. $prev = '<span class="calendar-prev"><a href="' . $prev_url . '">&laquo; ' . t('Prev') . '</a></span>';
  1678. // If we are already on TODAY's month and year, then there should be no Prev link at all.
  1679. if ($month == date('m') && $year == date('Y')) {
  1680. $prev = "&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;";
  1681. }
  1682. $next_month = $month + 1;
  1683. $next_year = $year;
  1684. if ($next_month > 12) {
  1685. $next_month = 1;
  1686. $next_year++;
  1687. }
  1688. $next_url = fp_url("schedule-appointment/$faculty_user_id", "stage=2&event_type_cid=$event_type_cid&month_year=$next_month" . "_" . "$next_year");
  1689. $next = '<span class="calendar-next"><a href="' . $next_url . '">' . t("Next") . ' &raquo;</a></span>';
  1690. // TODO: limit future months/dates based on settings? Ex: don't let the user go past 3 months.
  1691. $top_text = '<caption class="calendar-month">' . $prev . "<span class='month-title'>$month_title</span>" . $next . "</caption>";
  1692. $calendar = "<div class='calendar-mini-calendar'>
  1693. <table class='mini-calendar'>
  1694. $top_text
  1695. <tr class='day-names'>";
  1696. if($day_name_length)
  1697. { //if the day names should be shown ($day_name_length > 0)
  1698. //if day_name_length is >3, the full name of the day will be printed
  1699. foreach($day_names as $d) {
  1700. $calendar .= '<th class="mini-day-of-week" abbr="' . htmlentities($d) . '">' . htmlentities($day_name_length < 4 ? substr($d,0,$day_name_length) : $d) . '</th>';
  1701. }
  1702. $calendar .= "</tr>
  1703. <tr class='week'>";
  1704. }
  1705. if ($weekday > 0) {
  1706. for ($i = 0; $i < $weekday; $i++) {
  1707. $calendar .= "<td class='empty-day'>&nbsp;</td>"; //initial 'empty' days
  1708. }
  1709. }
  1710. /////////////////////////////////////////////////////
  1711. // Begin to output the actual days in the month.
  1712. $days_in_month = date('t',$first_of_month);
  1713. for ($day = 1; $day <= $days_in_month; $day++, $weekday++) {
  1714. $disp_day = $day;
  1715. if($weekday == 7) {
  1716. $weekday = 0; //start a new week
  1717. $calendar .= "</tr>
  1718. <tr class='week'>";
  1719. }
  1720. $extra = "";
  1721. if ($day === intval($today_num) && $month == $today_month && $year == $today_year) {
  1722. $extra .= " mini-day-today";
  1723. }
  1724. // has the date already passed?
  1725. if ($day < $today_num && $month == $today_month && $year == $today_year) {
  1726. $extra .= " mini-past-date";
  1727. }
  1728. else {
  1729. // We are on track to being able to make this a link!
  1730. $disp_day = $day;
  1731. // is the faculty member available? If so, make it a link
  1732. if (isset($available_slots[$year][$month][$day]) && is_array($available_slots[$year][$month][$day]) && count($available_slots[$year][$month][$day]) > 0) {
  1733. $disp_day = l($day, "schedule-appointment/$faculty_user_id", "month_year=$month_year&stage=3&day=$day&event_type_cid=$event_type_cid");
  1734. }
  1735. } // else
  1736. $calendar .= "<td class='mini-day $extra'>$disp_day</td>";
  1737. }
  1738. //////////////////////////////////////////////////////
  1739. // We are finished, but if we didn't end on the last day of the month,
  1740. // we need to fill in the remaining table cells with an empty td.
  1741. if($weekday != 7) {
  1742. $calendar .= '<td class="empty-day empty-day-multi" id="emptydays" colspan="' . (7-$weekday) . '">&nbsp;</td>'; //remaining "empty" days
  1743. }
  1744. // Time to close up!
  1745. $calendar .= "</tr>
  1746. </table>
  1747. </div>";
  1748. return $calendar;
  1749. }// calendar_render_mini_calendar
  1750. /**
  1751. * For use with the content module. We will register our custom content type(s)
  1752. * for use with this module.
  1753. */
  1754. function calendar_content_register_content_type() {
  1755. global $user;
  1756. $arr = array();
  1757. $arr['appointment'] = array(
  1758. 'title' => 'Appointment',
  1759. 'description' => 'This is a content type meant to track an appointment between a student and an advisor in the system.',
  1760. 'settings' => array(
  1761. 'title' => array(
  1762. 'auto' => TRUE,
  1763. ),
  1764. 'css' => fp_get_module_path("calendar") . "/css/style.css",
  1765. ),
  1766. );
  1767. $arr['schedule_event_type'] = array(
  1768. 'title' => 'Schedule Appointment Type',
  1769. 'description' => 'This is a content type is the type of appointment which a student can schedule. Ex: Phone Call, Meeting, etc.',
  1770. 'settings' => array(
  1771. 'title' => array(
  1772. 'label' => t("Appointment Type Title"),
  1773. 'description' => t("This is what the student will select for the type of appointment they whish to schedule with you.
  1774. <br>Ex: In-Person Meeting, Phone Call, Video Call"),
  1775. ),
  1776. 'css' => fp_get_module_path("calendar") . "/css/style.css",
  1777. ),
  1778. );
  1779. $arr['schedule_unavailable_time'] = array(
  1780. 'title' => 'Schedule Unavailable Time',
  1781. 'description' => 'This content type allows the user to mark when they will be unavailable for appointments.',
  1782. 'settings' => array(
  1783. 'title' => array(
  1784. 'label' => t("Title or Short Description"),
  1785. ),
  1786. 'js' => fp_get_module_path("calendar") . "/js/calendar.js",
  1787. 'css' => fp_get_module_path("calendar") . "/css/style.css",
  1788. ),
  1789. );
  1790. // If we are in a popup (dialog)...
  1791. if (@$_GET['window_mode'] == 'popup') {
  1792. // We want to make sure we redirect to our handler URL, which will close the dialog.
  1793. $arr['appointment']['settings']['#redirect'] = array(
  1794. 'path' => 'content-dialog-handle-after-save',
  1795. 'query' => '',
  1796. );
  1797. $arr['schedule_event_type']['settings']['#redirect'] = array(
  1798. 'path' => 'content-dialog-handle-after-save',
  1799. 'query' => '',
  1800. );
  1801. $arr['schedule_unavailable_time']['settings']['#redirect'] = array(
  1802. 'path' => 'content-dialog-handle-after-save',
  1803. 'query' => '',
  1804. );
  1805. }
  1806. $fields = array();
  1807. $fields['appointment_datetime'] = array(
  1808. 'type' => 'datetime-local',
  1809. 'label' => 'Date/Time',
  1810. 'value' => date('Y-m-d\TH:i', time()), // today's date/time by default
  1811. 'format_date' => 'short',
  1812. 'weight' => 0,
  1813. 'required' => TRUE,
  1814. );
  1815. $options = array();
  1816. for ($t = 5; $t <= 120; $t = $t + 5) {
  1817. $options[$t] = "$t minutes";
  1818. }
  1819. $fields['appointment_duration_minutes'] = array(
  1820. 'type' => 'select',
  1821. 'label' => 'Duration',
  1822. 'options' => $options,
  1823. 'hide_please_select' => TRUE,
  1824. 'weight' => 10,
  1825. 'required' => TRUE,
  1826. );
  1827. $fields['faculty_id'] = array(
  1828. 'type' => 'textfield',
  1829. 'label' => 'Faculty/Staff',
  1830. 'weight' => 20,
  1831. );
  1832. $fields['student_id'] = array(
  1833. 'type' => 'textfield',
  1834. 'label' => 'Student',
  1835. 'autocomplete_path' => 'student-search/autocomplete-student/default', // Convenient endpoint in the student_search module
  1836. 'weight' => 30,
  1837. );
  1838. $fields['appointment_type'] = array(
  1839. 'type' => 'select',
  1840. 'options' => array(
  1841. 'advising' => t('Advising'),
  1842. 'other' => t('Other'),
  1843. ),
  1844. 'label' => 'Type',
  1845. 'required' => TRUE,
  1846. 'hide_please_select' => TRUE,
  1847. 'weight' => 40,
  1848. );
  1849. $fields['appointment_msg'] = array(
  1850. 'type' => 'textarea',
  1851. 'label' => '(Optional) Message / Comments',
  1852. 'filter' => 'basic',
  1853. 'weight' => 50,
  1854. );
  1855. // This is essentially a hidden field, but we need it to be a textarea, so it can hold linebreaks and such.
  1856. // It's where we will save extra data regarding Zoom meetings, and possibly other video meeting services
  1857. // in the future.
  1858. $fields['video_data'] = array(
  1859. 'type' => 'textarea',
  1860. 'label' => '',
  1861. 'weight' => 999999,
  1862. 'attributes' => array('readonly' => 'readonly', "style" => "display: none"),
  1863. );
  1864. $arr['appointment']['fields'] = $fields;
  1865. ///////////////////////////////////
  1866. /// Schedule Event Type
  1867. ///////////////////////////////////
  1868. $fields = array();
  1869. $fields['faculty_id'] = array(
  1870. 'type' => 'textfield',
  1871. 'label' => 'Faculty/Staff',
  1872. 'weight' => -10,
  1873. );
  1874. $fields['enabled'] = array(
  1875. 'type' => 'select',
  1876. 'label' => t('Enabled?'),
  1877. 'description' => t("Set to Enabled in order to allow students to schedule this appointment type. If you disable all of your event types,
  1878. then no student will be able to make appointments with you. If you are unsure what to do, leave this set to 'Enabled'."),
  1879. 'options' => array('enabled' => 'Enabled', 'disabled' => 'Disabled'),
  1880. 'hide_please_select' => TRUE,
  1881. 'required' => TRUE,
  1882. 'weight' => 30,
  1883. );
  1884. $zoomapi_data = "";
  1885. if (module_enabled("zoomapi")) {
  1886. $zoomapi_data = zoomapi_get_zoomapi_data_for_user($user->id);
  1887. if (!$zoomapi_data) {
  1888. $attr = array("style" => "display: none; "); // hide this field if the user doesn't have anything set up for Zoom.
  1889. }
  1890. $fields['video_meeting'] = array(
  1891. 'type' => 'select',
  1892. 'label' => t('Automatically generate Zoom meeting?'),
  1893. 'options' => array('no' => t('No (default)'), 'zoom' => t('Yes - Automatically generate a Zoom meeting and attach URL')),
  1894. 'hide_please_select' => TRUE,
  1895. 'description' => t("If set to Yes, this event will be treated like a video chat, and a Zoom meeting will automatically be
  1896. created for the appointment. The URL for the new Zoom meeting will be included at the end of the notification
  1897. and reminder messages sent to the student & faculty/staff members."),
  1898. 'attributes' => $attr,
  1899. 'weight' => 40,
  1900. );
  1901. }
  1902. $fields['description'] = array(
  1903. 'type' => 'textarea',
  1904. 'label' => t('Brief Description'),
  1905. 'description' => t('Optional. This description will appear along side the title of this event. Provide a brief description to explain what this event type
  1906. is. For example, if this is for a Video Call, you might enter: <em>Select this option if you wish to meet via your phone or laptop camera, without appearing in-person.</em>'),
  1907. 'weight' => 50,
  1908. );
  1909. $fields['event_duration_minutes'] = array(
  1910. 'type' => 'select',
  1911. 'label' => 'Duration',
  1912. 'options' => array('15' => '15 min', '30' => '30 min', '45' => '45 min', '60' => '60 min', '75' => '1 hr, 15 min', '90' => '1 hr, 30 min', '105' => '1 hr, 45 min', '120' => '2 hours'),
  1913. 'hide_please_select' => TRUE,
  1914. 'weight' => 110,
  1915. 'required' => TRUE,
  1916. 'description' => t('How many minutes will this event last?'),
  1917. );
  1918. $fields['event_buffer_minutes'] = array(
  1919. 'type' => 'select',
  1920. 'label' => 'Buffer between scheduled appointments',
  1921. 'options' => array('5' => '5 min', '10' => '10 min', '15' => '15 min', '30' => '30 min', '45' => '45 min', '60' => '60 min', '75' => '1 hr, 15 min', '90' => '1 hr, 30 min', '105' => '1 hr, 45 min', '120' => '2 hours'),
  1922. 'hide_please_select' => TRUE,
  1923. 'weight' => 120,
  1924. 'required' => TRUE,
  1925. 'description' => t('How many minutes should there be from the end of one appointment or event, and the start of the next?'),
  1926. );
  1927. $options = array();
  1928. for ($t = 1; $t <= 24; $t++) {
  1929. $s = "s";
  1930. if ($t == 1) $s = "";
  1931. $options[$t] = "$t " . t("hour$s");
  1932. }
  1933. $fields['prevent_less_than_hours'] = array(
  1934. 'type' => 'select',
  1935. 'label' => 'Prevent events less than ___ hours away',
  1936. 'options' => $options,
  1937. 'hide_please_select' => TRUE,
  1938. 'weight' => 130,
  1939. 'required' => TRUE,
  1940. 'description' => t('This setting stops students from scheduling events too soon, which you might not notice because you are otherwise occupied.
  1941. For example, if you set this to 1 hour, and the current time is 2pm, then a student would not be able to schedule
  1942. this event before 3pm today.'),
  1943. );
  1944. $fields['additional_email_msg'] = array(
  1945. 'type' => 'textarea',
  1946. 'label' => t('Additional Email Message'),
  1947. 'description' => t('When a student schedules this event, they will see a generic email message which confirms their appointment date and time.
  1948. If you wish, you can add an additional message here.
  1949. <br>For example:
  1950. <br>&nbsp; &nbsp; <i>Thank you for scheduling this in-person meeting. My office is in Walker Hall, 3rd floor. Check in at the reception.</i>'),
  1951. 'weight' => 140,
  1952. );
  1953. $arr['schedule_event_type']['fields'] = $fields;
  1954. ///////////////////////////////////////
  1955. // schedule_unavailable_time
  1956. ///////////////////////////////////////
  1957. $fields = array();
  1958. $fields['faculty_id'] = array(
  1959. 'type' => 'textfield',
  1960. 'label' => 'Faculty/Staff',
  1961. 'weight' => -10,
  1962. );
  1963. $fields['days'] = array(
  1964. 'type' => 'checkboxes',
  1965. 'label' => 'Days affected',
  1966. 'options' => array(0 => 'Sun', 1 => 'Mon', 2 => 'Tue', 3 => 'Wed', 4 => 'Thu', 5 => 'Fri', 6=> 'Sat'),
  1967. 'description' => t("Select the days which this unavailable time entry refers to."),
  1968. 'weight' => 20,
  1969. );
  1970. $hour_options = array();
  1971. for ($t = 0; $t <= 23; $t++) {
  1972. $twelve_version = $t . "am";
  1973. if ($t == 0) {
  1974. $twelve_version = "12am (midnight)";
  1975. }
  1976. if ($t > 12) {
  1977. $twelve_version = ($t - 12) . "pm";
  1978. }
  1979. $hour_options[$t] = $twelve_version;
  1980. }
  1981. $fields['time_selector'] = array(
  1982. 'type' => 'radios',
  1983. 'label' => t('How would you like to describe when you are unavailable to take appointments on the selected days?'),
  1984. 'options' => array('' => 'Default', 'manual' => 'Manual Entry', 'none' => 'None (no appointments on these days)'),
  1985. 'weight' => 23,
  1986. );
  1987. $fields['day_start_hour'] = array(
  1988. 'type' => 'select',
  1989. 'label' => t('What time do you <u>begin</u> accepting appointments on the selected days?'),
  1990. 'options' => $hour_options,
  1991. 'required' => TRUE,
  1992. 'hide_please_select' => TRUE,
  1993. 'weight' => 25,
  1994. );
  1995. $fields['day_stop_hour'] = array(
  1996. 'type' => 'select',
  1997. 'label' => t('What time do you <u>stop</u> accepting appointments on the selected days?'),
  1998. 'options' => $hour_options,
  1999. 'required' => TRUE,
  2000. 'hide_please_select' => TRUE,
  2001. 'weight' => 25,
  2002. );
  2003. $fields['start_time'] = array(
  2004. 'type' => 'time',
  2005. 'label' => 'Start Time',
  2006. 'description' => t("Select the Start time for this unavailable window."),
  2007. 'weight' => 30,
  2008. );
  2009. $fields['end_time'] = array(
  2010. 'type' => 'time',
  2011. 'label' => 'End Time',
  2012. 'description' => t("Select the End time for this unavailable window."),
  2013. 'weight' => 40,
  2014. );
  2015. $fields['advanced_fs'] = array(
  2016. 'type' => 'cfieldset',
  2017. 'label' => 'Advanced Settings - click to view',
  2018. 'start_closed' => TRUE,
  2019. 'weight' => 50,
  2020. );
  2021. $fields['advanced_fs']['elements'][0]['ics_url'] = array(
  2022. 'type' => 'textarea',
  2023. 'rows' => 2,
  2024. 'label' => '(Advanced) URL to external calendar feed',
  2025. 'description' => t("If a URL is entered here, all other fields will be ignored. This optional field lets you specify the URL to an external
  2026. calendar feed (ex: Google Calendar, Outlook, Zoho, etc) in .ics format. Any event times listed on that calendar
  2027. will be marked off as unavailable for scheduling in FlightPath. This is a convenient way to prevent students
  2028. from scheduling appointments when you might otherwise be occupied, by entering a link to your work calendar.
  2029. <br>If you are unsure what to enter here, leave
  2030. this field blank."),
  2031. 'weight' => 60,
  2032. );
  2033. $arr['schedule_unavailable_time']['fields'] = $fields;
  2034. return $arr;
  2035. } // hook_content_register_content_type
  2036. function calendar_display_calendar() {
  2037. $rtn = "";
  2038. fp_set_title('');
  2039. $year_month = @trim($_REQUEST['year_month']);
  2040. if ($year_month == '') $year_month = 'now';
  2041. if ($year_month == 'now') {
  2042. $temp = strtotime('now');
  2043. $year_month = date('Y-m', $temp);
  2044. }
  2045. $temp = explode("-", $year_month);
  2046. $year = intval($temp[0]);
  2047. $month = intval($temp[1]);
  2048. // if mobile_date has been set, then display that (the entire day).
  2049. if (isset($_REQUEST['mobile_date'])) {
  2050. $rtn .= calendar_display_mobile_date_page($_REQUEST['mobile_date'], $_REQUEST['mobile_cids']);
  2051. }
  2052. else {
  2053. $rtn .= calendar_build_custom_calendar($month, $year);
  2054. }
  2055. watchdog("calendar", "display_calendar", array());
  2056. return $rtn;
  2057. } //calendar_display_calendar
  2058. /**
  2059. * This function is specifically for displaying the events on a particular day
  2060. * for the user, presumed to be in a mobile experience.
  2061. *
  2062. * The HTML we need to display is assumed to be in our SESSION[calendar_cache][date][cid]
  2063. *
  2064. */
  2065. function calendar_display_mobile_date_page($date, $cids_csv) {
  2066. $rtn = "";
  2067. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  2068. $date_ts = strtotime($date);
  2069. $month_desc = date('F', $date_ts);
  2070. $month = date('m', $date_ts);
  2071. $year = date('Y', $date_ts);
  2072. $day = date('d', $date_ts);
  2073. $ord = date('S', $date_ts);
  2074. $dow = date('l', $date_ts);
  2075. fp_set_title("$dow, $month_desc $day$ord, $year");
  2076. $cids = explode(",", $cids_csv);
  2077. $rtn .= "<div class='mobile-day-page'>";
  2078. foreach ($cids as $cid) {
  2079. $rtn .= $_SESSION['calendar_cache'][$date][$cid];
  2080. } // foreach
  2081. $rtn .= "</div>";
  2082. // Let's set our breadcrumbs
  2083. $db = get_global_database_handler();
  2084. $crumbs = array();
  2085. $crumbs[] = array(
  2086. 'text' => t("Calendar"),
  2087. 'path' => 'calendar',
  2088. 'query' => "month_year=$month" . "_$year",
  2089. );
  2090. fp_set_breadcrumbs($crumbs);
  2091. return $rtn;
  2092. }
  2093. /**
  2094. * Creating date collection between two dates
  2095. *
  2096. * <code>
  2097. * <?php
  2098. * # Example 1
  2099. * date_range("2014-01-01", "2014-01-20", "+1 day", "m/d/Y");
  2100. *
  2101. * # Example 2. you can use even time
  2102. * date_range("01:00:00", "23:00:00", "+1 hour", "H:i:s");
  2103. * </code>
  2104. *
  2105. * @author Ali OYGUR <alioygur@gmail.com>
  2106. * @param string since any date, time or datetime format
  2107. * @param string until any date, time or datetime format
  2108. * @param string step
  2109. * @param string date of output format
  2110. * @return array
  2111. */
  2112. function calendar_get_date_range($first, $last, $step = '+1 day', $output_format = 'Y-m-d' ) {
  2113. $dates = array();
  2114. $current = strtotime($first);
  2115. $last = strtotime($last);
  2116. while( $current <= $last ) {
  2117. $dates[] = date($output_format, $current);
  2118. $current = strtotime($step, $current);
  2119. }
  2120. return $dates;
  2121. }
  2122. /**
  2123. * Implements hook_form_alter
  2124. *
  2125. * We want to make various modifications to our form, based on what we are trying to do to it. We may also
  2126. * want to add in some custom javascript as well.
  2127. *
  2128. */
  2129. function calendar_form_alter(&$form, $form_id) {
  2130. global $user;
  2131. if ($form_id == 'content_edit_content_form') {
  2132. if (@$form['type']['value'] == 'appointment') {
  2133. $db = get_global_database_handler();
  2134. $form['#validate_handlers'][] = 'calendar_appointment_content_form_validate';
  2135. // If this is a NEW form, then check for values in the URL to auto-fill.
  2136. if ($form['cid']['value'] === 'new') {
  2137. if (isset($_GET['student_id'])) {
  2138. $form['student_id']['value'] = $_GET['student_id'];
  2139. $form['student_id']['attributes'] = array('class' => 'hidden');
  2140. $form['mark_to'] = array(
  2141. 'type' => 'markup',
  2142. 'value' => "<div class='appointment-field-mark appointment-student'><label>" . t("To") . ":</label>
  2143. " . $db->get_student_name($form['student_id']['value'], TRUE) . "
  2144. </div>",
  2145. 'weight' => $form['student_id']['weight'],
  2146. );
  2147. }
  2148. if (isset($_GET['faculty_id'])) {
  2149. $form['faculty_id']['value'] = $_GET['faculty_id'];
  2150. $form['faculty_id']['attributes'] = array('class' => 'hidden');
  2151. $form['mark_from'] = array(
  2152. 'type' => 'markup',
  2153. 'value' => "<div class='appointment-field-mark calendar-faculty_id'><label>" . t("Faculty/Staff") . ":</label>
  2154. " . $db->get_faculty_name($form['faculty_id']['value']) . "
  2155. </div>",
  2156. 'weight' => $form['faculty_id']['weight'],
  2157. );
  2158. }
  2159. // Set the date/time to NOW (since this is new).
  2160. $form['appointment_datetime']['value'] = date('Y-m-d\TH:i', convert_time(time()));
  2161. // If _GET['date'] is set, then we will set the appointment date to THAT + the current time.
  2162. // We pass it through strtotime to help sanitize.
  2163. if (isset($_GET['date']) && $_GET['date'] != '') {
  2164. $form['appointment_datetime']['value'] = date('Y-m-d', strtotime($_GET['date'])) . date('\TH:i', convert_time(time()));
  2165. }
  2166. // Always set the published = TRUE, and hide.
  2167. $form['published']['value'] = TRUE;
  2168. $form['published']['attributes'] = array('class' => 'hidden');
  2169. // If this user has Zoom set up, then show an option to make this a zoom meeting.
  2170. $test = FALSE;
  2171. if (module_enabled("zoomapi")) {
  2172. $test = zoomapi_get_zoomapi_data_for_user($user->id);
  2173. }
  2174. if ($test) {
  2175. $form['enable_zoom'] = array(
  2176. 'type' => 'checkbox',
  2177. 'label' => t("Make this a Zoom video meeting?"),
  2178. 'description' => t("Check this box if this is a Zoom video meeting. A Zoom meeting will be created automatically
  2179. and the recipients will be notified."),
  2180. 'weight' => 5,
  2181. );
  2182. }
  2183. } // cid == new
  2184. } // form type == appointment
  2185. if (@$form['type']['value'] == 'schedule_event_type') {
  2186. if ($form['cid']['value'] === 'new') {
  2187. $db = get_global_database_handler();
  2188. if (isset($_GET['faculty_id'])) {
  2189. $form['faculty_id']['value'] = $_GET['faculty_id'];
  2190. }
  2191. } // new
  2192. // Always set the published = TRUE, and hide.
  2193. $form['published']['value'] = TRUE;
  2194. $form['published']['attributes'] = array('class' => 'hidden');
  2195. // Always hide faculty_id field.
  2196. $form['faculty_id']['attributes'] = array('class' => 'hidden');
  2197. $form['mark_from'] = array(
  2198. 'type' => 'markup',
  2199. 'value' => "<div class='appointment-field-mark calendar-faculty_id'><label>" . t("Faculty/Staff") . ":</label>
  2200. " . fp_get_faculty_name($form['faculty_id']['value']) . "
  2201. </div>",
  2202. 'weight' => $form['faculty_id']['weight'],
  2203. );
  2204. } // schedule_event_type
  2205. if (@$form['type']['value'] == 'schedule_unavailable_time') {
  2206. if ($form['cid']['value'] === 'new') {
  2207. $db = get_global_database_handler();
  2208. if (isset($_GET['faculty_id'])) {
  2209. $form['faculty_id']['value'] = $_GET['faculty_id'];
  2210. }
  2211. // Set timezone to the user's timezone (or system if unavail)
  2212. $form['timezone']['value'] = fp_get_user_timezone();
  2213. } // new
  2214. // Always set the published = TRUE, and hide.
  2215. $form['published']['value'] = TRUE;
  2216. $form['published']['attributes'] = array('class' => 'hidden');
  2217. // Always hide faculty_id field.
  2218. $form['faculty_id']['attributes'] = array('class' => 'hidden');
  2219. $form['mark_from'] = array(
  2220. 'type' => 'markup',
  2221. 'value' => "<div class='appointment-field-mark calendar-faculty_id'><label>" . t("Faculty/Staff") . ":</label>
  2222. " . fp_get_faculty_name($form['faculty_id']['value']) . "
  2223. </div>",
  2224. 'weight' => $form['faculty_id']['weight'],
  2225. );
  2226. } // schedule_unavailable_time
  2227. } // content_edit_content_form
  2228. } // hook_form_alter
  2229. /**
  2230. * Custom validate handler for when we save an appointment form. We want to confirm the student is valid,
  2231. * and also store ONLY the cwid.
  2232. */
  2233. function calendar_appointment_content_form_validate($form, &$form_state) {
  2234. global $user;
  2235. $values = $form_state['values'];
  2236. $student_id = $line = $form_state['values']['student_id'];
  2237. // Might look like this: first last (CWID). We only want the CWID.
  2238. if (strstr($line, "(")) {
  2239. $temp = explode("(", $line);
  2240. $student_id = $temp[1];
  2241. $student_id = trim(str_replace(")", "", $student_id));
  2242. }
  2243. // $student_id should now contain only the CWID of the student. Let's make sure it's a real person, by making sure their name is entered.
  2244. $db = get_global_database_handler();
  2245. $name = trim($db->get_student_name($student_id, FALSE));
  2246. if (!$name) {
  2247. form_error('student_id', t("Sorry, but the Student (by CWID or Name) you entered is not valid. Please try again."));
  2248. }
  2249. // If we're good to go, it means we can add the new (stripped down) student_id to the form_state for saving.
  2250. $form_state['values']['student_id'] = $student_id;
  2251. // Since we have passed validation, we can see if we should try to create a new Zoom meeting.
  2252. $video_info = $location = "";
  2253. $acid = 0;
  2254. $location = "In-Person";
  2255. $faculty_user_id = intval($user->id);
  2256. $faculty_user = $user;
  2257. $faculty_cwid = $faculty_user->cwid;
  2258. $faculty_name = $faculty_user->f_name . " " . $faculty_user->l_name;
  2259. $faculty_tz = fp_get_user_timezone($user);
  2260. $student_name = $db->get_student_name($student_id);
  2261. $schedule_time_ts = strtotime($values['appointment_datetime']);
  2262. $utc_schedule_time_ts = convert_time($schedule_time_ts, $faculty_tz, 'UTC'); // convert to utc time from our current timezone.
  2263. $event_duration_minutes = intval($values['appointment_duration_minutes']);
  2264. $event_title = "Meeting between $student_name and $faculty_name";
  2265. // Only do this for NEW appointments.
  2266. if ($values['cid'] === 'new' && module_enabled('zoomapi') && isset($values['enable_zoom']) && $values['enable_zoom'] === TRUE) {
  2267. // We want to create a new Zoom meeting!
  2268. $event_title = t("Zoom meeting between @stu and @fac", array("@stu" => $student_name, "@fac" => $faculty_name));
  2269. $month = date('n', $schedule_time_ts);
  2270. $year = date('Y', $schedule_time_ts);
  2271. $day = date('j', $schedule_time_ts);
  2272. //gmt_date_time must look like yyyy-MM-ddTHH:mm:ssZ. For example: 2020-03-31T22:02:00Z. The Z tells it that we are using GMT/UTC time.
  2273. $gmt_date_time = date("Y-m-d\TH:i:s\Z", $utc_schedule_time_ts);
  2274. $response = zoomapi_create_zoom_meeting($faculty_user_id, $gmt_date_time, $event_duration_minutes, "Video meeting - $faculty_name / $student_name");
  2275. if (!$response) {
  2276. form_error("", t("Sorry, but there was an issue creating the Zoom meeting for this appointment. Your appointment has not been saved.
  2277. Please try another method, or contact the IT administrator to let them know of this issue."));
  2278. return;
  2279. }
  2280. $decoded = json_decode($response);
  2281. $meeting_id = $decoded->id;
  2282. $join_url = $decoded->join_url;
  2283. $location = $join_url;
  2284. $dial_in = $decoded->settings->global_dial_in_numbers[0]->number;
  2285. if (function_exists("engagements_convert_to_pretty_phone_number")) {
  2286. $dial_in = engagements_convert_to_pretty_phone_number($dial_in, TRUE);
  2287. }
  2288. $video_info .= "<p><b>" . t("Zoom Meeting:") . "</b> <a href='$join_url'>" . $join_url . "</a> \n<br> " . t("Be sure to download the Zoom app on your phone, tablet, or computer.") . " \n<br> " . t("To dial in instead of using video chat, you may dial:<br>\n @dial (ID: @id)", array("@dial" => $dial_in, "@id" => $meeting_id)) . ".</p>\n";
  2289. $decoded->video_info = $video_info; // add to our json object for ease of use later.
  2290. $decoded->video_provider = "zoom"; // declare that this is a "zoom" meeting.
  2291. $decoded->video_account_owner_user_id = $user->id;
  2292. $video_data = json_encode($decoded); // re-encode as a simpl string, so we can add to the database.
  2293. $form_state['values']['video_data'] = $video_data;
  2294. } // enable_zoom
  2295. // Send notifications
  2296. // Get ics invitation string for email attachments
  2297. $start_dt = date("Ymd\THis\Z", $utc_schedule_time_ts);
  2298. $end_dt = date("Ymd\THis\Z", $utc_schedule_time_ts + ($event_duration_minutes * 60));
  2299. $ics = calendar_get_ics_invitation_string($event_title, $location, $video_info, $start_dt, $end_dt);
  2300. $attachment = array(
  2301. 'invite.ics' => $ics,
  2302. );
  2303. // First, send a notification to the student.
  2304. $student_user_id = db_get_user_id_from_cwid($student_id, 'student');
  2305. $student_tz = fp_get_user_timezone($student_user_id);
  2306. $fstudent_tz = friendly_timezone($student_tz);
  2307. $nmsg = "You have been scheduled for an appointment with $faculty_name:\n";
  2308. $msg = "";
  2309. $msg .= "<p><b>" . t("Scheduled:") . "</b> $event_title</p>\n";
  2310. $msg .= "<p><b>" . t("Date and Time:") . "</b> " . format_date(convert_time($utc_schedule_time_ts, 'UTC', $student_tz), 'pretty') . " ($fstudent_tz)</p>\n";
  2311. $msg .= "<p><b>" . t("Duration:") . "</b> " . $event_duration_minutes . " min</p>\n";
  2312. $msg .= $video_info;
  2313. notify_send_notification_to_user($student_user_id, $nmsg . $msg, $acid, 'appointment', '', $attachment);
  2314. // Now, send a notification to the faculty member.
  2315. $ffaculty_tz = friendly_timezone($faculty_tz);
  2316. $nmsg = "You have scheduled an appointment with $student_name:\n";
  2317. $msg = "";
  2318. $msg .= "<p><b>" . t("Scheduled:") . "</b> $event_title</p>\n";
  2319. $msg .= "<p><b>" . t("Date and Time:") . "</b> " . format_date(convert_time($utc_schedule_time_ts, 'UTC', $faculty_tz), 'pretty') . " ($ffaculty_tz)</p>\n";
  2320. $msg .= "<p><b>" . t("Duration:") . "</b> " . $event_duration_minutes . " min</p>\n";
  2321. $msg .= $video_info;
  2322. notify_send_notification_to_user($user->id, $nmsg . $msg, $acid, 'appointment', '', $attachment);
  2323. } // validate
  2324. /**
  2325. * Return back an ics file (as a string) to be used as an attachment for emails, which
  2326. * will facilitate a calendar invitation.
  2327. *
  2328. * Inspired from: https://gist.github.com/jakebellacera/635416/3c81643cc236a5efdf535fcbf3f876eaaa6c4787
  2329. *
  2330. * start_dt and end_dt should look like Ymd\THis\Z (in UTC format)
  2331. *
  2332. */
  2333. function calendar_get_ics_invitation_string($event_title, $location, $description = "", $start_dt = 0, $end_dt = 0) {
  2334. $rtn = "";
  2335. if ($description == "") $description = $event_title;
  2336. $description = strip_tags($description);
  2337. $description = str_replace("\r\n", "\n", $description); // Convert from windows to unix style linebreaks.
  2338. $description = str_replace("\n", "\\n", $description); // required if we have line breaks in the description. We have to escape the slash.
  2339. $description = str_replace(";", "\\;", $description); // We have to escape semi-colons the same way
  2340. $description = str_replace(",", "\\,", $description); // And commas.
  2341. // We have to do "line folding" for the description if it is longer than 75 chars. See: https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html
  2342. $rtn .= "BEGIN:VCALENDAR\n";
  2343. $rtn .= "VERSION:2.0\n";
  2344. $rtn .= "PRODID:-//hacksw/handcal//NONSGML v1.0//EN\n";
  2345. $rtn .= "CALSCALE:GREGORIAN\n";
  2346. $rtn .= "BEGIN:VEVENT\n";
  2347. $rtn .= "DTEND:$end_dt\n";
  2348. $rtn .= "UID:" . md5($event_title . $start_dt . $end_dt) . "\n";
  2349. $rtn .= "DTSTAMP:" . date("Ymd\THis\Z", time()) . "\n";
  2350. $rtn .= "LOCATION:$location\n";
  2351. $rtn .= "DESCRIPTION:" . calendar_ics_split("DESCRIPTION:", $description) . "\n";
  2352. //$rtn .= "URL;VALUE=URI: http://mydomain.com/events/blah\n";
  2353. $rtn .= "SUMMARY:$event_title\n";
  2354. $rtn .= "DTSTART:$start_dt\n";
  2355. $rtn .= "END:VEVENT\n";
  2356. $rtn .= "END:VCALENDAR";
  2357. return $rtn;
  2358. } // get_ics_invitation_string
  2359. /**
  2360. * This lets us split up a line for ics into 75-char octets, according to the rules
  2361. * from: https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html
  2362. *
  2363. * This function comes from: https://gist.github.com/hugowetterberg/81747
  2364. */
  2365. function calendar_ics_split($preamble = "DESCRIPTION:", $value) {
  2366. $value = trim($value);
  2367. $value = strip_tags($value);
  2368. $value = preg_replace('/\n+/', ' ', $value);
  2369. $value = preg_replace('/\s{2,}/', ' ', $value);
  2370. $preamble_len = strlen($preamble);
  2371. $lines = array();
  2372. while (strlen($value)>(75-$preamble_len)) {
  2373. $space = (75-$preamble_len);
  2374. $mbcc = $space;
  2375. while ($mbcc) {
  2376. $line = mb_substr($value, 0, $mbcc);
  2377. $oct = strlen($line);
  2378. if ($oct > $space) {
  2379. $mbcc -= $oct-$space;
  2380. }
  2381. else {
  2382. $lines[] = $line;
  2383. $preamble_len = 1; // Still take the tab into account
  2384. $value = mb_substr($value, $mbcc);
  2385. break;
  2386. }
  2387. }
  2388. }
  2389. if (!empty($value)) {
  2390. $lines[] = $value;
  2391. }
  2392. return join("\n\t", $lines);
  2393. }
  2394. /**
  2395. * Actually renders the HTML for the calendar.
  2396. */
  2397. function calendar_build_custom_calendar($month, $year) {
  2398. global $user;
  2399. fp_add_css(fp_get_module_path('calendar') . '/css/style.css');
  2400. $extra = "";
  2401. $today = date('Y-m-d', convert_time(time()));
  2402. // Create array containing abbreviations of days of week.
  2403. $daysOfWeek = array('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
  2404. // What is the first day of the month in question?
  2405. $firstDayOfMonth = mktime(0,0,0,$month,1,$year);
  2406. $check_month = $month - 1;
  2407. $check_year = $year;
  2408. if ($check_month < 1) {
  2409. $check_year--;
  2410. $check_month = 12;
  2411. }
  2412. $last_day_of_prev_month = cal_days_in_month(CAL_GREGORIAN, $check_month, $check_year);
  2413. $original_month = intval($month);
  2414. // How many days does this month contain?
  2415. $numberDays = date('t',$firstDayOfMonth);
  2416. // Retrieve some information about the first day of the
  2417. // month in question.
  2418. $dateComponents = getdate($firstDayOfMonth);
  2419. // What is the name of the month in question?
  2420. $monthName = $dateComponents['month'];
  2421. $short_month_name = date('M', $dateComponents[0]);
  2422. // What is the index value (0-6) of the first day of the
  2423. // month in question.
  2424. $dayOfWeek = $dateComponents['wday'];
  2425. $calendar = "";
  2426. $prev_month = $month - 1;
  2427. $prev_year = $year;
  2428. if ($prev_month < 1) {
  2429. $prev_year--;
  2430. $prev_month = 12;
  2431. }
  2432. $prev_url = fp_url("calendar", "year_month=$prev_year-$prev_month");
  2433. $next_month = $month + 1;
  2434. $next_year = $year;
  2435. if ($next_month > 12) {
  2436. $next_year++;
  2437. $next_month = 1;
  2438. }
  2439. $next_url = fp_url("calendar", "year_month=$next_year-$next_month");
  2440. // Create the table tag opener and day headers
  2441. $calendar .= "<div class='calendar-header'><span class='cal-prev-link'><a href='$prev_url'>&laquo; Prev</a></span>$extra$short_month_name $year<span class='cal-next-link'><a href='$next_url'>Next &raquo;</a></span></div>";
  2442. $calendar .= "<table class='custom-calendar calendar-calendar calendar-month-" . strtoupper($monthName) . " calendar-weeks-displayed-zWEEKSDISPLAYEDz calendar-visible-$extra'>";
  2443. $calendar .= "<tr>";
  2444. // Create the calendar headers
  2445. foreach($daysOfWeek as $day) {
  2446. $calendar .= "<th class='header'>$day</th>";
  2447. }
  2448. // Create the rest of the calendar
  2449. $start_day = 1;
  2450. $start_month = $month;
  2451. $start_year = $year;
  2452. // If our month starts on any day OTHER than Sunday (0), then we need to include days at the end of the LAST month!
  2453. if ($dayOfWeek > 0) {
  2454. for ($t = $dayOfWeek; $t > 1; $t--) {
  2455. $last_day_of_prev_month--;
  2456. }
  2457. $start_day = $last_day_of_prev_month;
  2458. $start_month = intval($month) - 1;
  2459. if ($start_month < 1) {
  2460. $start_year--;
  2461. $start_month = 12;
  2462. }
  2463. $dayOfWeek = 0;
  2464. }
  2465. // Similarly, if the last day of this month falls on any other day than Saturday (6), we need to advance it to the NEXT month.
  2466. $to_day = $numberDays;
  2467. $to_month = $month;
  2468. $to_year = $year;
  2469. $last_w = date('w', strtotime("$to_year-$to_month-$to_day"));
  2470. if ($last_w < 6) {
  2471. $to_day = 1;
  2472. for ($t = $last_w + 1; $t < 6; $t++) {
  2473. $to_day++;
  2474. }
  2475. $to_month++;
  2476. if ($to_month > 12) {
  2477. $to_year++;
  2478. $to_month = 1;
  2479. }
  2480. }
  2481. $all_dates = calendar_get_date_range("$start_year-$start_month-$start_day", "$to_year-$to_month-$to_day");
  2482. // Initiate the day counter, starting with the 1st.
  2483. $currentDay = 1;
  2484. $calendar .= "</tr><tr>";
  2485. // The variable $dayOfWeek is used to
  2486. // ensure that the calendar
  2487. // display consists of exactly 7 columns.
  2488. if ($dayOfWeek > 0) {
  2489. $calendar .= "<td colspan='$dayOfWeek'>&nbsp;</td>";
  2490. }
  2491. $month = str_pad($month, 2, "0", STR_PAD_LEFT);
  2492. $next_month = str_pad($next_month, 2, "0", STR_PAD_LEFT);
  2493. $number_of_weeks_displayed = 1;
  2494. $db = get_global_database_handler();
  2495. $all_appointments = array();
  2496. // Get all of our activity dates into an array, so we can then convert them to local timezone, to figure out what dates they belong on.
  2497. $res = db_query("SELECT DISTINCT(a.cid) FROM content__appointment a, content n
  2498. WHERE a.vid = n.vid
  2499. AND a.cid = n.cid
  2500. AND delete_flag = 0
  2501. AND (field__appointment_datetime LIKE ? OR field__appointment_datetime LIKE ?)
  2502. AND (field__faculty_id = ? OR field__student_id = ?)
  2503. ORDER BY field__appointment_datetime ASC", array("$year-$month%", "$next_year-$next_month%", $user->cwid, $user->cwid)); // Because of stupid timezones, I need to grab from the NEXT month as well, just in case.
  2504. while ($cur = db_fetch_object($res)) {
  2505. $cid = $cur->cid;
  2506. $content = content_load($cid);
  2507. $mobile_day_cids[] = $cid;
  2508. $db_date_ts = strtotime($content->field__appointment_datetime['value']); // This is in UTC
  2509. $converted_date_ts = convert_time($db_date_ts); // Convert to our local timezone
  2510. $start_date = date('Y-m-d', $converted_date_ts);
  2511. $start_time = date('g:ia', $converted_date_ts);
  2512. $duration_min = $content->field__appointment_duration_minutes['value'];
  2513. $end_time = format_date(strtotime("$start_date $start_time + $duration_min MINUTES"), 'just_time');
  2514. $all_appointments[$start_date][$start_time][] = array(
  2515. 'db_date_ts' => $db_date_ts,
  2516. 'converted_date_ts' => $converted_date_ts,
  2517. 'start_date' => $start_date,
  2518. 'start_time' => $start_time,
  2519. 'cid' => $cid,
  2520. 'content' => $content,
  2521. );
  2522. } // while
  2523. foreach ($all_dates as $the_date) {
  2524. $extra_class = "";
  2525. $temp = explode("-", $the_date);
  2526. $year = $temp[0];
  2527. $month = intval($temp[1]);
  2528. $month = str_pad($month, 2, "0", STR_PAD_LEFT);
  2529. if (intval($month) != $original_month) {
  2530. $extra_class .= " out-of-range-day";
  2531. }
  2532. $currentDay = intval($temp[2]);
  2533. // Seventh column (Saturday) reached. Start a new row.
  2534. if ($dayOfWeek == 7) {
  2535. $number_of_weeks_displayed++;
  2536. $dayOfWeek = 0;
  2537. $calendar .= "</tr><tr>";
  2538. }
  2539. $currentDayRel = str_pad($currentDay, 2, "0", STR_PAD_LEFT);
  2540. $date = "$year-$month-$currentDayRel";
  2541. $corrected_date = date('Y-m-d', strtotime($date));
  2542. if ($date == $today) {
  2543. $extra_class .= " today";
  2544. }
  2545. $add_link = "";
  2546. if (user_has_permission('add_appointment_content')) {
  2547. $url = fp_url('content/add/appointment', "window_mode=popup&date=$date&faculty_id=$user->cwid");
  2548. $add_link = "<a class='add-date' href='javascript:fpOpenLargeIframeDialog(\"$url\", \"Add New Appointment\", \"\");'><i class='fa fa-plus'></i></a>";
  2549. }
  2550. $calendar .= "<td class='day $extra_class' rel='$date'><span class='date-box-top'>$add_link<span class='the-day-num'>$currentDay</span></span>";
  2551. $mobile_day_count = 0;
  2552. $mobile_day_cids = array();
  2553. $html = "";
  2554. if (isset($all_appointments[$date])) {
  2555. foreach ($all_appointments[$date] as $start_time => $XX) {
  2556. foreach ($all_appointments[$date][$start_time] as $c => $details) {
  2557. $start_date = $date;
  2558. $cid = $details['cid'];
  2559. $content = $details['content'];
  2560. $mobile_day_cids[] = $cid;
  2561. $duration_min = $content->field__appointment_duration_minutes['value'];
  2562. $end_time = format_date(strtotime("$start_date $start_time + $duration_min MINUTES"), 'just_time');
  2563. $student_name = fp_get_student_name($content->field__student_id['value']);
  2564. if (!$student_name) {
  2565. $student_name = fp_get_faculty_name($content->field__student_id['value']);
  2566. }
  2567. $faculty_name = fp_get_faculty_name($content->field__faculty_id['value']);
  2568. $apt_title = $faculty_name . " - " . $student_name;
  2569. if ($student_name == $faculty_name) {
  2570. $apt_title = $student_name;
  2571. }
  2572. // Let's make the apt_title be a link to open the appointment.
  2573. $view_url = fp_url("content/$cid", "window_mode=popup&content_tabs=false");
  2574. $dtitle = t("View Appointment");
  2575. $view_link = "<a href='javascript:fpOpenLargeIframeDialog(\"$view_url\",\"$dtitle\");' title='" . t("View") . "' >$apt_title</a>";
  2576. $time_line = "<div class='time-line'>$start_time - $end_time</div>";
  2577. $appt_extra_class = "";
  2578. if ($content->published == 0) {
  2579. $appt_extra_class = "appt-canceled";
  2580. $view_link = "<span title='This appointment was canceled.'>$apt_title</span>"; // no longer a link, since it was canceled
  2581. }
  2582. $html = "<div class='calendar-entry $appt_extra_class'>
  2583. <div class='name-shift-line'>$view_link</div>
  2584. $time_line
  2585. </div>";
  2586. $_SESSION['calendar_cache'][$date][$cid] = $html; // save to our SESSION, we may need it later in a mobile context.
  2587. $calendar .= $html;
  2588. $mobile_day_count++;
  2589. }
  2590. }
  2591. }
  2592. if ($mobile_day_count > 0) {
  2593. //$url = fp_url('calendar', "year_month=$year_month&mobile_date=$date&mobile_cids=" . join(",", $mobile_day_cids));
  2594. $url = fp_url('calendar', "mobile_date=$date&mobile_cids=" . join(",", $mobile_day_cids));
  2595. $calendar .= "<div style='display: none;' class='mobile-day-count'>
  2596. <a href='" . $url . "'>$mobile_day_count</a>
  2597. </div>";
  2598. }
  2599. $calendar .= '</td>';
  2600. // Increment counters
  2601. //$currentDay++;
  2602. $dayOfWeek++;
  2603. }
  2604. // Complete the row of the last week in month, if necessary
  2605. if ($dayOfWeek != 7) {
  2606. $remainingDays = 7 - $dayOfWeek;
  2607. $calendar .= "<td colspan='$remainingDays'>&nbsp;</td>";
  2608. }
  2609. $calendar .= "</tr>";
  2610. $calendar .= "</table>";
  2611. $calendar = str_replace("zWEEKSDISPLAYEDz", $number_of_weeks_displayed, $calendar);
  2612. return $calendar;
  2613. } // calendar_build_custom_calendar
  2614. /**
  2615. * Implements hook_content_alter. We want to alter the way that our calendar appointments are displayed.
  2616. */
  2617. function calendar_content_alter(&$render, $render_id) {
  2618. global $user;
  2619. if (strstr($render_id, "content_")) {
  2620. if (@$render['#content-type'] == 'appointment') {
  2621. $cid = str_replace("content_", "", $render_id);
  2622. $content = content_load($cid);
  2623. $student_name = @fp_get_student_name($content->field__student_id['value'], TRUE);
  2624. $faculty_name = @fp_get_faculty_name($content->field__faculty_id['value']);
  2625. // Replace our rendered values with actual names, instead of ONLY cwids.
  2626. $render['field__faculty_id']['value'] = "<div class='field-value'>$faculty_name</div>";
  2627. $render['field__student_id']['value'] = "<div class='field-value'>$student_name</div>";
  2628. // Make a nice title for this appointment.
  2629. $apt_title = t("Meeting between @ft and @st on @dt", array("@ft" => $faculty_name, "@st" => $student_name, "@dt" => $content->field__appointment_datetime['display_value']));
  2630. $render['content_title']['value'] = "<div class='content-title'>$apt_title</div>";
  2631. fp_set_title($apt_title); // set the page title in case we are viewing not in a dialog
  2632. // If the user has permission to cancel appointments involving them, then show link!
  2633. if (user_has_permission("can_cancel_own_appointments")) {
  2634. if ($user->cwid == $content->field__student_id['value'] || $user->cwid == $content->field__faculty_id['value'] || $user->id == 1) {
  2635. // if we are in an iframe, include that in query
  2636. $query = "";
  2637. if (isset($_REQUEST['window_mode'])) {
  2638. $query .= "window_mode=" . $_REQUEST['window_mode'];
  2639. }
  2640. $cancel_link = fp_url("calendar/confirm-cancel-appointment/$content->cid", $query);
  2641. $render['cancel_link'] = array(
  2642. 'value' => "<a href='$cancel_link' title='Cancel appointment' class='button cancel-button'>" . t("Cancel Appointment?") . "</a>",
  2643. 'weight' => 99999,
  2644. );
  2645. } // if user is involved in this, or they are admin.
  2646. } // user has permission
  2647. // If we have valid video data, we should display relevant links to join the meeting and such.
  2648. unset($render['field__video_data']);
  2649. if ($render['#content_object'] && $render['#content_object']->field__video_data['value'] != "") {
  2650. // Meaning, we have video meeting data to display!
  2651. // create new markup field which renders out the video links and such.
  2652. $decoded = json_decode($render['#content_object']->field__video_data['value']);
  2653. $meeting_id = $decoded->id;
  2654. $join_url = $decoded->join_url;
  2655. $dial_in = $decoded->settings->global_dial_in_numbers[0]->number;
  2656. if (function_exists("engagements_convert_to_pretty_phone_number")) {
  2657. $dial_in = engagements_convert_to_pretty_phone_number($dial_in, TRUE);
  2658. }
  2659. $video_info .= "<p><b>" . t("Zoom Meeting:") . "</b> <a href='$join_url'>" . $join_url . "</a> \n<br> " . t("Be sure to download the Zoom app on your phone, tablet, or computer.") . " \n<br> " . t("To dial in instead of using video chat, you may dial:<br>\n @dial (ID: @id)", array("@dial" => $dial_in, "@id" => $meeting_id)) . ".</p>\n";
  2660. $render['video_information'] = array(
  2661. 'type' => 'markup',
  2662. 'value' => "<div class='video-information'>" . $video_info . "</div>",
  2663. 'weight' => 65,
  2664. );
  2665. } // video_data != ""
  2666. } // this is type appointment
  2667. } // this probably is a content node view
  2668. } // hook_content_alter

Functions

Namesort descending Description
calendar_access_can_cancel_appointment Make sure the user is allowed to cancel this appointment.
calendar_appointment_content_form_validate Custom validate handler for when we save an appointment form. We want to confirm the student is valid, and also store ONLY the cwid.
calendar_appointment_settings_form Lets an admin user configure global settings regarding appointments in FlightPath
calendar_build_custom_calendar Actually renders the HTML for the calendar.
calendar_confirm_cancel_appointment_form Confirm we actually want to cancel this appointment
calendar_confirm_cancel_appointment_form_submit
calendar_content_alter Implements hook_content_alter. We want to alter the way that our calendar appointments are displayed.
calendar_content_register_content_type For use with the content module. We will register our custom content type(s) for use with this module.
calendar_cron Implements hook_cron
calendar_display_calendar
calendar_display_mobile_date_page This function is specifically for displaying the events on a particular day for the user, presumed to be in a mobile experience.
calendar_display_schedule_appointment_completed_page The user has successfully completed their appointment scheduling. Display a Thank You page.
calendar_display_schedule_appointment_page This is the page which lets students schedule an appointment with the faculty member supplied in the user_id.
calendar_display_schedule_staff_page This page (primarily meant for students) is for quickly finding your advisor or professor or whomever, and finding their link to schedule an appointment with them.
calendar_display_upcoming_appointments
calendar_display_user_appointment_settings_page This page is where the user can configure their various appointment settings (like when they offer them)
calendar_find_and_remind_notify_upcoming_appointments This function will find appointments approaching within X number of minutes, and send out notifications to all involved.
calendar_form_alter Implements hook_form_alter
calendar_get_appointments_for_faculty Return back a list of appointment content nodes for this faculty member, which fall between the specified datetimes.
calendar_get_available_faculty_schedule Returns back an array of time slots available for this faculty member and event_type
calendar_get_available_times_on_date
calendar_get_date_range Creating date collection between two dates
calendar_get_ics_invitation_string Return back an ics file (as a string) to be used as an attachment for emails, which will facilitate a calendar invitation.
calendar_get_upcoming_appointments_for_cwid Returns an array of upcoming appointments, where the user is specified by CWID. start_date and end_date is meant to be in UTC, in the form of Y-m-d
calendar_ics_split This lets us split up a line for ics into 75-char octets, according to the rules from: https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html
calendar_menu implements hook_menu
calendar_perm
calendar_render_mini_calendar
calendar_schedule_appointment_confirm_form The confirmation form the user will see once they have made their schedule selections.
calendar_schedule_appointment_confirm_form_submit We passed validation, it's time to actually submit now!
calendar_schedule_appointment_confirm_form_validate This is our last chance to validate the form before saving.