PluginProbe
Easy Appointments / 4.0
Easy Appointments v4.0
4.0.2.1 4.0.2 4.0.1 3.12.27 3.12.28 3.12.29 4.0 3.12.26 3.12.25 trunk 2.0.0 2.1.0 2.1.1 2.1.3 2.1.4 2.10.0 2.2.0 2.2.3 2.2.4 2.3.0 2.3.1 2.3.11 2.3.2 2.3.3 2.3.4 All 79 releases
easy-appointments / src / logic.php

logic.php in Easy Appointments 4.0, at src/logic.php

594 lines 20.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // If this file is called directly, abort.
4 if (!defined('WPINC')) {
5 die;
6 }
7
8
9 /**
10 * Class responsible for App logic
11 * reservation, free times...
12 */
13 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
14 class EALogic
15 {
16
17 /**
18 * @var EADBModels
19 */
20 protected $models;
21
22 /**
23 * @var EAOptions
24 */
25 protected $options;
26
27 /**
28 * @var wpdb
29 */
30 protected $wpdb;
31
32 /**
33 * @var array Cache for services
34 */
35 protected $service_cache = [];
36
37 /**
38 * @var EASlotsLogic
39 */
40 protected $slots_logic;
41
42 /**
43 * EALogic constructor.
44 * @param wpdb $wpdb
45 * @param EADBModels $models
46 * @param EAOptions $options
47 */
48 function __construct($wpdb, $models, $options, $slots_logic)
49 {
50 $this->wpdb = $wpdb;
51 $this->models = $models;
52 $this->options = $options;
53 $this->slots_logic = $slots_logic;
54 }
55
56 /**
57 * Get all open slots / times
58 *
59 * @param int $location Location
60 * @param int $service Service
61 * @param int $worker Worker
62 * @param datetime $day DateTime
63 * @param int $app_id Previus appointment
64 * @param bool $check_current_day Previus appointment
65 * @param int $block_before
66 * @return array Array of free times
67 */
68 public function oldget_open_slots(
69 $location = null,
70 $service = null,
71 $worker = null,
72 $day = null,
73 $app_id = null,
74 $check_current_day = true,
75 $block_before = 0
76 )
77 {
78 // current day as weekday now (string)
79 $day_of_week = gmdate('l', strtotime($day));
80
81 // get current datetime as int
82 $time_now = current_time('timestamp', false);
83
84 // add block minutes
85 $block_time = $time_now + intval($block_before) * 60;
86
87 // calculate if that is current day that we are looking
88 $is_current_day = (gmdate('Y-m-d') == $day);
89
90 $block_date = gmdate('Y-m-d', $block_time);
91 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
92 $query = $this->wpdb->prepare("SELECT * FROM {$this->wpdb->prefix}ea_connections WHERE
93 location=%d AND
94 service=%d AND
95 worker=%d AND
96 day_of_week LIKE %s AND
97 is_working = 1 AND
98 (day_from IS NULL OR day_from <= %s) AND
99 (day_to IS NULL OR day_to >= %s)",
100 $location, $service, $worker, "%{$day_of_week}%", $day, $day
101 );
102 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
103 $open_days = $this->wpdb->get_results($query);
104
105 $working_hours = array();
106
107 $serviceObj = $this->get_service($service);
108
109 /**
110 * Example on Appointment 08:00 - 20:00 today
111 */
112 foreach ($open_days as $working_day) {
113 // upper time 20:00;
114 $upper_time = strtotime($working_day->time_to);
115
116 $counter = 0;
117
118 while (true) {
119 // 08:00 at first
120 $temp_time = strtotime($working_day->time_from);
121 $diff_between_duration_and_slot_step = 0;
122
123 // use smaller step
124 if (!empty($serviceObj->slot_step)) {
125 $run_time = $serviceObj->slot_step * 60 * $counter++;
126 $diff_between_duration_and_slot_step = ($serviceObj->duration - $serviceObj->slot_step) * 60;
127 } else {
128 $run_time = $serviceObj->duration * 60 * $counter++;
129 }
130
131 // 08:00 at first pass, second 09:00
132 $temp_time += $run_time;
133
134 $temp_date_time = strtotime("$day {$working_day->time_from}") + $run_time;
135
136 // is that before upper time limit (slot end time must not exceed working hours end time)
137 if (($temp_time + ($serviceObj->duration * 60)) <= $upper_time) {
138 $current_time = gmdate('H:i', $temp_time);
139
140 // check if current time is greater then slot start time
141 if ($check_current_day && $is_current_day && $time_now > $temp_time) {
142 continue;
143 }
144
145 // block time - skip if it is under block time
146 if ($block_before > 0 && $check_current_day && $block_time > $temp_date_time) {
147 continue;
148 }
149
150 // slot count
151 $slot_count = is_numeric($working_day->slot_count) ? (int) $working_day->slot_count : 1;
152
153 if (!array_key_exists($current_time, $working_hours)) {
154 $working_hours[$current_time] = $slot_count;
155 } else {
156 $working_hours[$current_time] += $slot_count;
157 }
158 } else {
159 break;
160 }
161 }
162 }
163
164 $service_duration = $serviceObj->duration;
165
166 // remove non-working time
167 $this->remove_closed_slots($working_hours, $location, $service, $worker, $day, $serviceObj->duration);
168
169 // remove already reserved times
170 $this->remove_reserved_slots($working_hours, $location, $service, $worker, $day, $service_duration, $app_id, $serviceObj->block_before, $serviceObj->block_after);
171
172 // format time
173 return $this->format_time($working_hours, $serviceObj->duration);
174 }
175
176 public function get_open_slots(
177 $location = null,
178 $service = null,
179 $worker = null,
180 $day = null,
181 $app_id = null,
182 $check_current_day = true,
183 $block_before = 0
184 )
185 {
186 $day_of_week = gmdate('l', strtotime($day));
187
188 $time_now = current_time('timestamp', false);
189 $block_time = $time_now + intval($block_before) * 60;
190 $is_current_day = (gmdate('Y-m-d') == $day);
191 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
192 $query = $this->wpdb->prepare("SELECT * FROM {$this->wpdb->prefix}ea_connections WHERE
193 location=%d AND
194 service=%d AND
195 worker=%d AND
196 is_working = 1 AND
197 (day_from IS NULL OR day_from <= %s) AND
198 (day_to IS NULL OR day_to >= %s)",
199 $location, $service, $worker, $day, $day
200 );
201 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
202 $open_days = $this->wpdb->get_results($query);
203
204 $working_hours = array();
205 $serviceObj = $this->get_service($service);
206
207 $day_name_map = [
208 'Sunday' => 0, 'Monday' => 1, 'Tuesday' => 2,
209 'Wednesday' => 3, 'Thursday' => 4, 'Friday' => 5, 'Saturday' => 6
210 ];
211
212 foreach ($open_days as $working_day) {
213 $allowed_days = array_map('trim', explode(',', $working_day->day_of_week));
214
215 if (!in_array($day_of_week, $allowed_days)) {
216 continue;
217 }
218
219 $repeat_week = (int)$working_day->repeat_week;
220 $day_from = !empty($working_day->day_from) ? $working_day->day_from : $day;
221 $current_date = new DateTime($day);
222 if ($repeat_week > 1) {
223 $target_dow = $day_name_map[$day_of_week];
224 $start_date = new DateTime($day_from);
225 while ((int)$start_date->format('w') !== $target_dow) {
226 $start_date->modify('+1 day');
227 }
228
229 $interval = $start_date->diff($current_date);
230 $weeks_between = floor($interval->days / 7);
231
232 if ($weeks_between % $repeat_week !== 0) {
233 continue;
234 }
235 }
236
237 $upper_time = strtotime($working_day->time_to);
238 $counter = 0;
239
240 while (true) {
241 $temp_time = strtotime($working_day->time_from);
242 $diff_between_duration_and_slot_step = 0;
243
244 if (!empty($serviceObj->slot_step)) {
245 $run_time = $serviceObj->slot_step * 60 * $counter++;
246 $diff_between_duration_and_slot_step = ($serviceObj->duration - $serviceObj->slot_step) * 60;
247 } else {
248 $run_time = $serviceObj->duration * 60 * $counter++;
249 }
250
251 $temp_time += $run_time;
252 $temp_date_time = strtotime("$day {$working_day->time_from}") + $run_time;
253
254 // is that before upper time limit (slot end time must not exceed working hours end time)
255 if (($temp_time + ($serviceObj->duration * 60)) <= $upper_time) {
256 $current_time = gmdate('H:i', $temp_time);
257
258 if ($check_current_day && $is_current_day && $time_now > $temp_time) {
259 continue;
260 }
261
262 if ($block_before > 0 && $check_current_day && $block_time > $temp_date_time) {
263 continue;
264 }
265
266 $slot_count = is_numeric($working_day->slot_count) ? (int) $working_day->slot_count : 1;
267
268 if (!array_key_exists($current_time, $working_hours)) {
269 $working_hours[$current_time] = $slot_count;
270 } else {
271 $working_hours[$current_time] += $slot_count;
272 }
273 } else {
274 break;
275 }
276 }
277 }
278
279 $service_duration = $serviceObj->duration;
280
281 $this->remove_closed_slots($working_hours, $location, $service, $worker, $day, $serviceObj->duration);
282 $this->remove_reserved_slots($working_hours, $location, $service, $worker, $day, $service_duration, $app_id, $serviceObj->block_before, $serviceObj->block_after);
283
284 return $this->format_time($working_hours, $serviceObj->duration);
285 }
286
287
288
289
290 /**
291 * Remove times when is not working
292 *
293 * @param array &$slots Free slots
294 * @param int $location Location
295 * @param int $service Service
296 * @param int $worker Worker
297 * @param DateTime $day DateTime
298 * @param time $service_duration Service duration in minuts
299 * @return null
300 */
301 private function remove_closed_slots(&$slots, $location = null, $service = null, $worker = null, $day = null, $service_duration = 60)
302 {
303 $day_of_week = gmdate('l', strtotime($day));
304 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
305 $query = $this->wpdb->prepare("SELECT * FROM {$this->wpdb->prefix}ea_connections WHERE
306 location=%d AND
307 service=%d AND
308 worker=%d AND
309 day_of_week LIKE %s AND
310 is_working = 0 AND
311 (day_from IS NULL OR day_from <= %s) AND
312 (day_to IS NULL OR day_to >= %s)",
313 $location, $service, $worker, "%{$day_of_week}%", $day, $day
314 );
315 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
316 $closed_days = $this->wpdb->get_results($query);
317
318
319 // check all no working times
320 foreach ($closed_days as $working_day) {
321
322 $lower_time = strtotime($working_day->time_from);
323 $upper_time = strtotime($working_day->time_to);
324
325 $counter = 0;
326
327 // check slots
328 foreach ($slots as $temp_time => $value) {
329 $current_time = strtotime($temp_time);
330 // $current_time_end = strtotime("$temp_time + $service_duration minute");
331 $current_time_end = strtotime("$temp_time + $service_duration minute");
332
333 if ($lower_time < $current_time && $upper_time <= $current_time) {
334 // before
335 } else if ($lower_time >= $current_time_end && $upper_time > $current_time_end) {
336 // after
337 } else {
338 // remove slot
339 $slot_count = is_numeric($working_day->slot_count) ? (int) $working_day->slot_count : 1;
340 $slots[$temp_time] = $value - $slot_count;
341 }
342 }
343 }
344 }
345
346 /**
347 * Can make reservation for that ip
348 *
349 * @param $data
350 * @return bool Can make reservation
351 */
352 public function can_make_reservation($data)
353 {
354 $ip = $data['ip'];
355
356 $result = array(
357 'status' => true,
358 'message' => ''
359 );
360 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
361 $query = $this->wpdb->prepare("SELECT id AS no_apps FROM {$this->wpdb->prefix}ea_appointments WHERE
362 ip=%s AND
363 status IN ('abandoned', 'pending') AND
364 created >= now() - INTERVAL 1 DAY",
365 $ip
366 );
367 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
368 $appIds = $this->wpdb->get_col($query);
369
370 $maxNumber = (int) $this->options->get_option_value('max.appointments', 10);
371
372 if (count($appIds) >= $maxNumber) {
373 $result['status'] = false;
374 $result['message'] = $maxNumber . __('Daily limit of booking request has been reached. Please contact us by email!', 'easy-appointments');
375 }
376
377 $result = apply_filters( 'easy_ea_can_make_reservation', $result, $data);
378
379 return $result;
380 }
381
382 public function can_update_reservation($appointment, $data)
383 {
384 $result = array(
385 'status' => true,
386 'message' => ''
387 );
388
389 $result = apply_filters( 'easy_ea_can_update_reservation', $result, $appointment, $data);
390
391 return $result;
392 }
393
394 /**
395 * Can make reservation for that User
396 *
397 * @param $data
398 * @return bool Can make reservation by user
399 */
400 public function can_make_reservation_by_user($data)
401 {
402 $result = array(
403 'status' => true,
404 'message' => ''
405 );
406
407 $maxNumber = (int) $this->options->get_option_value('max.appointments_by_user', 10);
408 if ($maxNumber > 0) {
409 $user = $data['user'];
410 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
411 $query = $this->wpdb->prepare("SELECT id AS no_apps FROM {$this->wpdb->prefix}ea_appointments WHERE
412 user=%s AND
413 status IN ('abandoned', 'pending') AND
414 created >= now() - INTERVAL 1 DAY",
415 $user
416 );
417 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
418 $appIds = $this->wpdb->get_col($query);
419
420 $maxNumber = (int) $this->options->get_option_value('max.appointments_by_user', 10);
421
422 if (count($appIds) >= $maxNumber) {
423 $result['status'] = false;
424 $result['message'] = $maxNumber." " . __('Daily limit of booking request has been reached. Please contact us by email!', 'easy-appointments');
425 }
426
427 $result = apply_filters( 'easy_ea_can_make_reservation', $result, $data);
428 }
429
430 return $result;
431 }
432
433 /**
434 * Remove times that are reserved (already booked)
435 *
436 * @param array &$slots Free slots
437 * @param int $location Location
438 * @param int $service Service
439 * @param int $worker Worker
440 * @param DateTime $day DateTime
441 * @param int $service_duration Service duration in minutes
442 * @param int $app_id
443 */
444 private function remove_reserved_slots(&$slots, $location, $service, $worker, $day, $service_duration, $app_id = -1, $block_before = 0, $block_after = 0)
445 {
446 if ($app_id == "") {
447 $app_id = -1;
448 }
449
450 $query = $this->slots_logic->get_busy_slot_query($location, $service, $worker, $day, $app_id);
451 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared
452 $appointments = $this->wpdb->get_results($query);
453
454 // dailyLimit section
455 $currentService = $this->get_service($service);
456 $limit = $currentService->daily_limit ? (int) $currentService->daily_limit : 0;
457 $serviceCount = 0;
458 foreach ($appointments as $app) {
459 if ($service === $app->service) {
460 $serviceCount++;
461 }
462 }
463 $limitReached = $limit > 0 && $limit <= $serviceCount;
464 // dailyLimit section end
465
466 // check all no working times
467 foreach ($appointments as $app) {
468 $start = ($app->date == $day) ? $app->start : '00:00';
469 $end = ($app->end_date == $day) ? $app->end : '23:59';
470
471 $lower_time = strtotime($start);
472 $upper_time = strtotime($end);
473
474 $serviceObj = $this->get_service($app->service);
475 // add block before and after time
476 if (!empty($serviceObj)) {
477 $lower_time -= ($serviceObj->block_before * 60);
478 $upper_time += ($serviceObj->block_after * 60);
479 }
480
481 // check slots
482 foreach ($slots as $temp_time => $value) {
483 // if we reached daily limit no point to go to calculation
484 if ($limitReached) {
485 $slots[$temp_time] = 0;
486 continue;
487 }
488
489 $slot_time = strtotime($temp_time);
490 $slot_time_end = strtotime("$temp_time + $service_duration minute");
491
492 // before / after
493 if (($slot_time_end + $block_after * 60) <= $lower_time || $upper_time <= ($slot_time - $block_before * 60)) { } else {
494 if ($this->slots_logic->is_exclusive_mode() && $this->slots_logic->is_provider_is_busy($app, $location, $service)) {
495 $slots[$temp_time] = 0;
496 continue;
497 }
498
499 // Cross time - remove one slot
500 $slots[$temp_time] = $value - 1;
501 }
502 }
503 }
504 }
505
506 /**
507 * Return service
508 *
509 * @param $service_id
510 * @return array|null|object|void
511 */
512 public function get_service($service_id)
513 {
514 if (array_key_exists($service_id, $this->service_cache)) {
515 return $this->service_cache[$service_id];
516 }
517
518 $model = $this->models->get_row('ea_services', $service_id);
519
520 $this->service_cache[$service_id] = $model;
521
522 return $model;
523 }
524
525 /**
526 * Get all statuses
527 *
528 * @return array
529 */
530 public function getStatus()
531 {
532 return array(
533 'pending' => __('pending', 'easy-appointments'),
534 'reservation' => __('reservation', 'easy-appointments'),
535 'abandoned' => __('abandoned', 'easy-appointments'),
536 'canceled' => __('cancelled', 'easy-appointments'),
537 'confirmed' => __('confirmed', 'easy-appointments'),
538 );
539 }
540
541 /**
542 * Translation for current statu
543 */
544 public function get_status_translation($status)
545 {
546 $statusCollection = $this->getStatus();
547
548 if (array_key_exists($status, $statusCollection)) {
549 return $statusCollection[$status];
550 }
551
552 return '';
553 }
554
555 /**
556 * Time format function
557 *
558 * @param array &$times Array of slots
559 * @param int $service_duration
560 * @return array Result times array
561 */
562 public function format_time(&$times, $service_duration)
563 {
564 $result = array();
565
566 $format = $this->options->get_option_value('time_format');
567
568 foreach ($times as $time => $count) {
569 switch ($format) {
570 case '00-24':
571 $result[] = array(
572 'count' => $count,
573 'value' => $time,
574 'show' => $time,
575 'ends' => gmdate('G:i', strtotime("{$time} + $service_duration minute"))
576 );
577 break;
578 case 'am-pm':
579 $result[] = array(
580 'count' => $count,
581 'value' => $time,
582 'show' => gmdate( 'h:i a', strtotime($time)),
583 'ends' => gmdate('h:i a', strtotime("{$time} + $service_duration minute"))
584 );
585 break;
586 default:
587 $result[] = $time;
588 break;
589 }
590 }
591
592 return $result;
593 }
594 }