PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.17
Booktics – Appointment Booking Calendar for Service Businesses v1.0.17
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
booktics / core / appointment / schedule / time-slot-manager.php

time-slot-manager.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.17, at core/appointment/schedule/time-slot-manager.php

432 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Time_Slot_Manager class
4 *
5 * @package Booktics\Appointment\Schedule
6 */
7
8 namespace Booktics\Appointment\Schedule;
9
10 use Booktics\Models\Appointment_Model;
11 use Booktics\Models\Team_Member_Model;
12 use Booktics\Services\Schedule_Record;
13 use DateTime;
14 use Exception;
15
16 /**
17 * Class Time_Slot_Manager
18 */
19 class Time_Slot_Manager {
20 /**
21 * Store Service Schedule Object
22 *
23 * @var Service_Schedule
24 */
25 private $service_schedule;
26
27 /**
28 * Store Team member Schedule Object
29 *
30 * @var Team_Member_Schedule
31 */
32 private $team_member_schedule;
33
34 /**
35 * Store Default Schedule Object
36 *
37 * @var Default_Schedule
38 */
39 private $default_schedule;
40
41 /**
42 * Store service duration in minutes
43 *
44 * @var integer
45 */
46 private $service_duration;
47
48 /**
49 * Store slot length in minutes
50 *
51 * @var integer
52 */
53 private $slot_length;
54
55 /**
56 * Store before buffer time in minutes
57 *
58 * @var integer
59 */
60 private $before_buffer_time;
61
62 /**
63 * Store after buffer time in minutes
64 *
65 * @var integer
66 */
67 private $after_buffer_time;
68
69 /**
70 * Store time format (12 or 24 hour)
71 *
72 * @var string
73 */
74 private $slot_format = '12'; // Default to 12 hour format.
75
76 /**
77 * Store service id
78 */
79 private $service_id = null;
80
81 /**
82 * Number of attendees for time slot
83 */
84 private $total_attendees = 1;
85
86 /**
87 * Constructor
88 *
89 * @param Service_Schedule $service Service schedule object.
90 * @param Team_Member_Schedule $team_member Team member schedule object.
91 * @param array $options Optional parameters as an associative array.
92 */
93 public function __construct( Service_Schedule $service, Team_Member_Schedule $team_member, $options = array() ) {
94 $defaults = array(
95 'default_schedule' => null,
96 'service_duration' => 30,
97 'slot_length' => 30,
98 'before_buffer_time' => 0,
99 'after_buffer_time' => 0,
100 'total_attendees' => 1,
101 );
102 $args = \wp_parse_args( $options, $defaults );
103
104 $this->service_schedule = $service;
105 $this->team_member_schedule = $team_member;
106 $this->default_schedule = $args['default_schedule'];
107 $this->service_duration = $args['service_duration'];
108 $this->slot_length = $args['slot_length'];
109 $this->before_buffer_time = $args['before_buffer_time'];
110 $this->after_buffer_time = $args['after_buffer_time'];
111 $this->service_id = $args['service_id'] ?? null;
112 $this->total_attendees = $args['total_attendees'];
113 }
114
115 /**
116 * Remove duplicates and sort time slots
117 *
118 * @param array $slots Array of time slots to deduplicate and sort.
119 * @return array Deduplicated and sorted array of time slots
120 */
121 private function deduplicate_and_sort_slots( array $slots ): array {
122 // Remove duplicates based on start and end times
123 $unique = array();
124 $seen = array();
125
126 foreach ( $slots as $slot ) {
127 $key = $slot->start . '-' . $slot->end;
128 if ( ! isset( $seen[ $key ] ) ) {
129 $seen[ $key ] = true;
130 $unique[] = $slot;
131 }
132 }
133
134 // Sort slots by start time
135 usort(
136 $unique,
137 function ( $a, $b ) {
138 $time_a = strtotime( $a->start );
139 $time_b = strtotime( $b->start );
140 return $time_a - $time_b;
141 }
142 );
143
144 return $unique;
145 }
146
147 /**
148 * Generate time slots for a specific date
149 *
150 * @param DateTime $date
151 *
152 * @return array
153 */
154 public function generate_slots( DateTime $date ): array {
155 $service_entries = $this->service_schedule->get_first_date( $date );
156 $team_member_entries = $this->team_member_schedule->get_first_date( $date );
157 $default_entries = $this->default_schedule ? $this->default_schedule->get_first_date( $date ) : array();
158
159 $all_slots = array();
160
161 // Handle the case when any schedule returns a single entry (backwards compatibility)
162 if ( ! is_array( $default_entries ) ) {
163 $default_entries = array( $default_entries );
164 }
165 if ( ! is_array( $service_entries ) ) {
166 $service_entries = array( $service_entries );
167 }
168 if ( ! is_array( $team_member_entries ) ) {
169 $team_member_entries = array( $team_member_entries );
170 }
171
172 // Remove null entries
173 $default_entries = array_filter( $default_entries );
174 $service_entries = array_filter( $service_entries );
175 $team_member_entries = array_filter( $team_member_entries );
176
177 // Get the appropriate strategy from factory and generate slots
178 $strategy = Time_Slot_Strategy_Factory::get_strategy(
179 $this->service_id,
180 $this->team_member_schedule->get_id() ?? null
181 );
182
183 $slots = $strategy->generate_slots(
184 $service_entries,
185 $team_member_entries,
186 $default_entries,
187 $this,
188 $date
189 );
190 $all_slots = array_merge( $all_slots, $slots );
191
192 // Remove duplicates and sort slots
193 $all_slots = $this->deduplicate_and_sort_slots( $all_slots );
194 $all_slots = $this->set_holiday_slots_as_empty_array( $all_slots );
195
196 return $all_slots;
197 }
198
199 /**
200 * Generate time slots for a date range.
201 *
202 * Creates an array of available time slots for each day in the given date range.
203 * The slots are marked as booked if they overlap with existing appointments.
204 *
205 * @param string $start_date Start date for the date range in Y-m-d format
206 * @param string $end_date End date for the date range in Y-m-d format
207 *
208 * @return array Collection of daily time slots indexed by date in Y-m-d format
209 * @throws Exception When date parsing fails or invalid date range is provided
210 */
211 public function generate_slots_for_range( string $start_date, string $end_date ): array {
212 $slots_by_day = array();
213 $start = new DateTime( $start_date );
214 $end = new DateTime( $end_date );
215
216 while ( $start <= $end ) {
217 $daily_slots = $this->generate_slots( clone $start );
218 $slots_by_day[ $start->format( 'Y-m-d' ) ] = $this->mark_booked_slots( clone $start, $daily_slots );
219 $start->modify( '+1 day' );
220 }
221
222 $slots_by_day = $this->set_holiday_slots_as_empty_array( $slots_by_day );
223
224 return $slots_by_day;
225 }
226
227 /**
228 * Removes slots that fall on holidays from the given array of generated slots by date.
229 *
230 * @param array $slots Array of time slots to remove holiday slots from.
231 * @return array Array of time slots with holiday slots removed.
232 */
233 private function set_holiday_slots_as_empty_array( array $slots ): array {
234 $team_member_holidays = [];
235 $holidays = booktics_get_option( 'holiday' , []); // holidays from default settings
236 $team_member_id = $this->team_member_schedule->get_id() ?? null;
237
238 if ( $team_member_id ) {
239 $team_member_holidays = ( new Schedule_Record() )->get_schedules( $team_member_id )['holiday'] ?? [];
240 $team_member = ( new Team_Member_Model( $team_member_id ) );
241
242 // If team member has custom holidays enabled, merge them with default holidays
243 if ( $team_member->enable_custom_holiday ) {
244 $holidays = array_merge( $holidays, $team_member_holidays );
245 }
246 }
247
248 if ( empty( $holidays ) || empty( $slots ) ) {
249 return $slots;
250 }
251
252 foreach ( $slots as $slot_date => $slot_data ) {
253 if ( empty( $slot_data ) ) {
254 continue;
255 }
256
257 if ( ! is_array( $holidays ) || empty( $holidays ) ) {
258 break;
259 }
260
261 foreach ( $holidays as $holiday ) {
262 $formatted_slot_date = gmdate( 'Y-m-d', strtotime( $slot_date ) );
263 $holiday_start = gmdate( 'Y-m-d', strtotime( $holiday[0] ) );
264 $holiday_end = gmdate( 'Y-m-d', strtotime( $holiday[1] ) );
265
266 // if the slot date is between holiday range, remove it by setting slots as empty
267 if ( $formatted_slot_date >= $holiday_start && $formatted_slot_date <= $holiday_end ) {
268 $slots[ $slot_date ] = [];
269 break;
270 }
271 }
272 }
273
274 return $slots;
275 }
276
277 /**
278 * Get all available slots
279 *
280 * @param DateTime $date
281 *
282 * @return array
283 * @throws Exception
284 */
285 public function get_available_slots( DateTime $date ): array {
286 $slots = $this->generate_slots( $date );
287 $slots = $this->mark_booked_slots( $date, $slots );
288
289 return array_filter( $slots, fn( $slot ) => $slot->is_available );
290 }
291
292 /**
293 * Mark booked slots
294 *
295 * @param DateTime $date
296 * @param array $slots
297 *
298 * @return array
299 * @throws Exception
300 */
301 public function mark_booked_slots( DateTime $date, array $slots ): array {
302 $booked_slots = $this->get_booked_slots( $date );
303 foreach ( $slots as $slot ) {
304 $slot_start = strtotime( $slot->start );
305 $slot_end = strtotime( $slot->end );
306 $booked_count = 0;
307
308 foreach ( $booked_slots as $booked ) {
309 $booked_start = strtotime( $booked['start'] );
310 $booked_end = strtotime( $booked['end'] );
311 if ( $slot_start < $booked_end && $slot_end > $booked_start ) {
312 if ( $booked['team_member_id'] === $this->team_member_schedule->get_id() ) {
313 $attendees = ! empty( $booked['total_attendees'] ) ? (int) $booked['total_attendees'] : 1;
314 $booked_count += $attendees;
315 }
316 }
317 }
318
319 $slot->booked_count = $booked_count;
320 $slot->available_spots = max( 0, $slot->max_capacity - $booked_count );
321 $slot->is_available = $slot->available_spots >= $this->total_attendees;
322 }
323
324 return $slots;
325 }
326
327 /**
328 * Get booked slots
329 *
330 * @param DateTime $date
331 *
332 * @return array
333 * @throws Exception
334 */
335 private function get_booked_slots( DateTime $date ): array {
336 $appointments = ( new Appointment_Model() )
337 ->get_appointments_for_slot(
338 $date->format( 'Y-m-d' ),
339 $this->team_member_schedule->get_id()
340 );
341
342 return array_map(
343 function ( $appointment ) use ( $date ) {
344 $start_time = strtotime( $appointment->start_time );
345 $appointment_duration = 30;
346 if ( $appointment->duration ) {
347 $appointment_duration = $appointment->duration;
348 }
349 $actual_end_time = $start_time + ( $appointment_duration * 60 );
350
351 return array(
352 'start' => gmdate( 'H:i', $start_time ),
353 'end' => gmdate( 'H:i', $actual_end_time ),
354 'service_id' => $appointment->service_id,
355 'team_member_id' => $appointment->team_member_id,
356 'total_attendees'=> $appointment->total_attendees ?? 1,
357 );
358 },
359 $appointments
360 );
361 }
362
363 /**
364 * Get the length of time slots in minutes
365 *
366 * @return integer Slot length in minutes
367 */
368 public function get_slot_length(): int {
369 return $this->slot_length;
370 }
371
372 /**
373 * Get the service duration in minutes
374 *
375 * @return integer Service duration in minutes
376 */
377 public function get_service_duration(): int {
378 return $this->service_duration;
379 }
380
381 /**
382 * Get the before buffer time in minutes
383 *
384 * @return integer Before buffer time in minutes
385 */
386 public function get_before_buffer_time(): int {
387 return $this->before_buffer_time;
388 }
389
390 /**
391 * Get the after buffer time in minutes
392 *
393 * @return integer After buffer time in minutes
394 */
395 public function get_after_buffer_time(): int {
396 return $this->after_buffer_time;
397 }
398
399 /**
400 * Get the total slot duration including buffer times
401 *
402 * @return integer Total slot duration in minutes
403 */
404 public function get_total_slot_duration(): int {
405 return $this->service_duration + $this->before_buffer_time + $this->after_buffer_time;
406 }
407
408 /**
409 * Gets service max capacity for a single time slot
410 * Its used to get services group booking max capacity
411 */
412 public function get_service_max_capacity_for_timeslot(): int {
413 // For handling cases where bookitcs pro is not active/ available
414 if ( ! class_exists( '\BookticsPro\Models\Group_Booking_Model' ) ) {
415 return 1;
416 }
417
418 $group_bookings_list = ( new \BookticsPro\Models\Group_Booking_Model() )->where( 'service_id', $this->service_id );
419 if ( ! is_array( $group_bookings_list ) ) {
420 return 1;
421 }
422
423 $group_booking = $group_bookings_list[0];
424
425 if ( ! $group_booking || ! $group_booking->enable_group_booking ) {
426 return 1;
427 }
428
429 return $group_booking->max_booking ?? 1;
430 }
431 }
432