PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 1.2.25
Booking for Appointments and Events Calendar – Amelia v1.2.25
2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.4.1 2.4 trunk 1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 1.2.17 1.2.18 1.2.19 1.2.2 1.2.20 1.2.21 1.2.22 1.2.23 1.2.24 1.2.25 1.2.26 1.2.27 1.2.28 1.2.29 1.2.3 1.2.30 1.2.31 1.2.32 1.2.33 1.2.34 1.2.35 1.2.36 1.2.37 1.2.38 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 2.0 2.0.1 2.0.2 2.1 2.1.1 2.1.2 2.1.3 2.2 2.2.1 2.3
ameliabooking / src / Domain / Services / TimeSlot / TimeSlotService.php
ameliabooking / src / Domain / Services / TimeSlot Last commit date
TimeSlotService.php 1 year ago
TimeSlotService.php
1240 lines
1 <?php
2
3 namespace AmeliaBooking\Domain\Services\TimeSlot;
4
5 use AmeliaBooking\Domain\Common\Exceptions\InvalidArgumentException;
6 use AmeliaBooking\Domain\Entity\Booking\Appointment\Appointment;
7 use AmeliaBooking\Domain\Entity\Booking\Appointment\CustomerBooking;
8 use AmeliaBooking\Domain\Entity\Booking\SlotsEntities;
9 use AmeliaBooking\Domain\Entity\Schedule\DayOff;
10 use AmeliaBooking\Domain\Entity\Bookable\Service\Service;
11 use AmeliaBooking\Domain\Entity\User\Provider;
12 use AmeliaBooking\Domain\Services\DateTime\DateTimeService;
13 use AmeliaBooking\Domain\Collection\Collection;
14 use AmeliaBooking\Domain\Services\Entity\EntityService;
15 use AmeliaBooking\Domain\Services\Interval\IntervalService;
16 use AmeliaBooking\Domain\Services\Resource\AbstractResourceService;
17 use AmeliaBooking\Domain\Services\Schedule\ScheduleService;
18 use AmeliaBooking\Domain\Services\User\ProviderService;
19 use AmeliaBooking\Domain\ValueObjects\String\BookingStatus;
20 use AmeliaBooking\Domain\ValueObjects\String\Status;
21 use DateInterval;
22 use DatePeriod;
23 use DateTime;
24 use DateTimeZone;
25 use Exception;
26
27 /**
28 * Class TimeSlotService
29 *
30 * @package AmeliaBooking\Domain\Services\TimeSlot
31 */
32 class TimeSlotService
33 {
34 /** @var IntervalService */
35 private $intervalService;
36
37 /** @var ScheduleService */
38 private $scheduleService;
39
40 /** @var ProviderService */
41 private $providerService;
42
43 /** @var AbstractResourceService */
44 private $resourceService;
45
46 /** @var EntityService */
47 private $entityService;
48
49 /**
50 * TimeSlotService constructor.
51 *
52 * @param IntervalService $intervalService
53 * @param ScheduleService $scheduleService
54 * @param ProviderService $providerService
55 * @param AbstractResourceService $resourceService
56 * @param EntityService $entityService
57 */
58 public function __construct(
59 IntervalService $intervalService,
60 ScheduleService $scheduleService,
61 ProviderService $providerService,
62 AbstractResourceService $resourceService,
63 EntityService $entityService
64 ) {
65 $this->intervalService = $intervalService;
66
67 $this->scheduleService = $scheduleService;
68
69 $this->providerService = $providerService;
70
71 $this->resourceService = $resourceService;
72
73 $this->entityService = $entityService;
74 }
75
76 /** @noinspection MoreThanThreeArgumentsInspection */
77 /**
78 * get appointment intervals for provider.
79 *
80 * @param array $weekDaysIntervals
81 * @param array $intervals
82 * @param string $dateString
83 * @param int $start
84 * @param int $end
85 * @return array
86 */
87 private function getModifiedEndInterval($weekDaysIntervals, &$intervals, $dateString, $start, $end)
88 {
89 $dayIndex = DateTimeService::getDayIndex($dateString);
90
91 if (isset($weekDaysIntervals[$dayIndex]['busy'][$start]) &&
92 $weekDaysIntervals[$dayIndex]['busy'][$start][1] > $end
93 ) {
94 $end = $weekDaysIntervals[$dayIndex]['busy'][$start][1];
95 }
96
97 if (isset($intervals[$dateString]['occupied'][$start]) &&
98 $intervals[$dateString]['occupied'][$start][1] > $end
99 ) {
100 $end = $intervals[$dateString]['occupied'][$start][1];
101 }
102
103 return $end;
104 }
105
106 /**
107 * Split start and end in array of dates.
108 *
109 * @param DateTime $start
110 * @param DateTime $end
111 *
112 * @return array
113 */
114 private function getPeriodDates($start, $end)
115 {
116 /** @var DatePeriod $period */
117 $period = new DatePeriod(
118 $start->setTime(0, 0, 0),
119 new DateInterval('P1D'),
120 $end
121 );
122
123 $periodDates = [];
124
125 /** @var DateTime $date */
126 foreach ($period as $index => $date) {
127 $periodDates[] = $date->format('Y-m-d');
128 }
129
130 return $periodDates;
131 }
132
133 /** @noinspection MoreThanThreeArgumentsInspection */
134 /**
135 * get appointment intervals for provider.
136 *
137 * @param Provider $provider
138 * @param Collection $locations
139 * @param int $serviceId
140 * @param int $locationId
141 * @param int $personsCount
142 * @param boolean $bookIfPending
143 * @param boolean $bookOverApp
144 * @param array $weekDaysIntervals
145 * @param array $specialDaysIntervals
146 * @return array
147 * @throws InvalidArgumentException
148 */
149 private function getProviderAppointmentIntervals(
150 $provider,
151 $locations,
152 $serviceId,
153 $locationId,
154 $personsCount,
155 $bookIfPending,
156 $bookOverApp,
157 &$weekDaysIntervals,
158 &$specialDaysIntervals
159 ) {
160 $intervals = [];
161
162 $specialDays = [];
163
164 foreach ($specialDaysIntervals as $specialDay) {
165 $specialDays = array_merge($specialDays, $specialDay['dates']);
166 }
167
168 /** @var Appointment $app */
169 foreach ($provider->getAppointmentList()->getItems() as $app) {
170 $occupiedStart = $provider->getTimeZone() ?
171 DateTimeService::getDateTimeObjectInTimeZone(
172 $app->getBookingStart()->getValue()->format('Y-m-d H:i'),
173 $provider->getTimeZone()->getValue()
174 ) : DateTimeService::getCustomDateTimeObject($app->getBookingStart()->getValue()->format('Y-m-d H:i'));
175
176 $occupiedEnd = $provider->getTimeZone() ?
177 DateTimeService::getDateTimeObjectInTimeZone(
178 $app->getBookingEnd()->getValue()->format('Y-m-d H:i'),
179 $provider->getTimeZone()->getValue()
180 ) : DateTimeService::getCustomDateTimeObject($app->getBookingEnd()->getValue()->format('Y-m-d H:i'));
181
182 if ($app->getServiceId()->getValue()) {
183 $occupiedStart->modify('-' . ($app->getService()->getTimeBefore() ? $app->getService()->getTimeBefore()->getValue() : 0) . ' seconds');
184
185 $occupiedEnd->modify('+' . ($app->getService()->getTimeAfter() ? $app->getService()->getTimeAfter()->getValue() : 0) . ' seconds');
186 }
187
188 $occupiedDateStart = $occupiedStart->format('Y-m-d');
189
190 $occupiedSecondsStart = $this->intervalService->getSeconds($occupiedStart->format('H:i') . ':00');
191
192 $occupiedSecondsEnd = $this->intervalService->getSeconds($occupiedEnd->format('H:i:s'));
193
194 if ($occupiedDateStart === $occupiedEnd->format('Y-m-d') &&
195 (!$bookOverApp || !$app->getServiceId()->getValue())
196 ) {
197 $intervals[$occupiedDateStart]['occupied'][$occupiedSecondsStart] = [
198 $occupiedSecondsStart,
199 $this->getModifiedEndInterval(
200 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
201 $intervals,
202 $occupiedDateStart,
203 $occupiedSecondsStart,
204 $occupiedSecondsEnd
205 )
206 ];
207 } else if (!$bookOverApp || !$app->getServiceId()->getValue()) {
208 $dates = $this->getPeriodDates($occupiedStart, $occupiedEnd);
209
210 $datesCount = sizeof($dates);
211
212 if ($datesCount === 1) {
213 $intervals[$dates[0]]['occupied'][$occupiedSecondsStart] = [
214 $occupiedSecondsStart,
215 $occupiedSecondsEnd === 0 ? 86400 : $occupiedSecondsEnd
216 ];
217 } else {
218 foreach ($dates as $index => $date) {
219 if ($index === 0) {
220 $intervals[$date]['occupied'][$occupiedSecondsStart] = [$occupiedSecondsStart, 86400];
221 } elseif ($index === $datesCount - 1) {
222 $modifiedEnd = $this->getModifiedEndInterval(
223 !array_key_exists($occupiedDateStart, $specialDays) ? $weekDaysIntervals : [],
224 $intervals,
225 $date,
226 0,
227 $occupiedSecondsEnd
228 );
229
230 $intervals[$date]['occupied'][0] = [
231 0,
232 $modifiedEnd === 0 ? 86400 : $modifiedEnd
233 ];
234 } else {
235 $intervals[$date]['occupied'][0] = [0, 86400];
236 }
237 }
238 }
239 }
240
241 $providerLocationId = $provider->getLocationId() ? $provider->getLocationId()->getValue() : null;
242
243 if ($app->getServiceId()->getValue() === $serviceId) {
244 $persons = 0;
245
246 /** @var CustomerBooking $booking */
247 foreach ($app->getBookings()->getItems() as $booking) {
248 $persons += $booking->getPersons()->getValue();
249 }
250
251 $status = $app->getStatus()->getValue();
252
253 $appLocationId = $app->getLocationId() ? $app->getLocationId()->getValue() : null;
254
255 $hasCapacity =
256 $personsCount !== null &&
257 ($persons + $personsCount) <= $app->getService()->getMaxCapacity()->getValue() &&
258 !($app->isFull() ? $app->isFull()->getValue() : false);
259
260 $hasLocation =
261 !$locationId ||
262 ($app->getLocationId() && $app->getLocationId()->getValue() === $locationId) ||
263 (!$app->getLocationId() && $providerLocationId === $locationId) ||
264 ($appLocationId &&
265 $appLocationId === $locationId &&
266 $locations->getItem($appLocationId)->getStatus()->getValue() === Status::VISIBLE) ||
267 (!$appLocationId && $providerLocationId &&
268 $locations->getItem($providerLocationId)->getStatus()->getValue() === Status::VISIBLE);
269
270 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
271
272 if (($hasLocation && $status === BookingStatus::APPROVED && $hasCapacity) ||
273 ($hasLocation && $status === BookingStatus::PENDING && ($bookIfPending || $hasCapacity))
274 ) {
275 $endDateTime = $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s');
276
277 $endDateTimeParts = explode(' ', $endDateTime);
278
279 $intervals[$occupiedDateStart]['available'][$app->getBookingStart()->getValue()->format('H:i')] =
280 [
281 'locationId' => $app->getLocationId() ?
282 $app->getLocationId()->getValue() : $providerLocationId,
283 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
284 'endDate' => $endDateTimeParts[0],
285 'endTime' => $endDateTimeParts[1],
286 'serviceId' => $serviceId,
287 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
288 ];
289 } else {
290 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
291 [
292 'locationId' => $app->getLocationId() ?
293 $app->getLocationId()->getValue() : $providerLocationId,
294 'places' => $app->getService()->getMaxCapacity()->getValue() - $persons,
295 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
296 'serviceId' => $app->getServiceId()->getValue(),
297 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
298 ];
299 }
300 } elseif ($app->getServiceId()->getValue()) {
301 $duration = $app->getBookingStart()->getValue()->diff($app->getBookingEnd()->getValue());
302
303 $intervals[$occupiedDateStart]['full'][$app->getBookingStart()->getValue()->format('H:i')] =
304 [
305 'locationId' => $app->getLocationId() ?
306 $app->getLocationId()->getValue() : $providerLocationId,
307 'places' => 0,
308 'end' => $app->getBookingEnd()->getValue()->format('Y-m-d H:i:s'),
309 'serviceId' => $app->getServiceId()->getValue(),
310 'duration' => ($duration->days * 24 * 60) + ($duration->h * 60) + $duration->i,
311 ];
312 }
313 }
314
315 return $intervals;
316 }
317
318 /**
319 * get provider day off dates.
320 *
321 * @param Provider $provider
322 *
323 * @return array
324 * @throws Exception
325 */
326 private function getProviderDayOffDates($provider)
327 {
328 $dates = [];
329
330 /** @var DayOff $dayOff */
331 foreach ($provider->getDayOffList()->getItems() as $dayOff) {
332 $endDateCopy = clone $dayOff->getEndDate()->getValue();
333
334 $dayOffPeriod = new DatePeriod(
335 $dayOff->getStartDate()->getValue(),
336 new DateInterval('P1D'),
337 $endDateCopy->modify('+1 day')
338 );
339
340 /** @var DateTime $date */
341 foreach ($dayOffPeriod as $date) {
342 $dateFormatted = $dayOff->getRepeat()->getValue() ? $date->format('m-d') : $date->format('Y-m-d');
343
344 $dates[$dateFormatted] = $dateFormatted;
345 }
346 }
347
348 return $dates;
349 }
350
351 /**
352 * get available appointment intervals.
353 *
354 * @param array $availableIntervals
355 * @param array $unavailableIntervals
356 *
357 * @return array
358 */
359 private function getAvailableIntervals(&$availableIntervals, $unavailableIntervals)
360 {
361 $parsedAvailablePeriod = [];
362
363 ksort($availableIntervals);
364 ksort($unavailableIntervals);
365
366 foreach ($availableIntervals as $available) {
367 $parsedAvailablePeriod[] = $available;
368
369 foreach ($unavailableIntervals as $unavailable) {
370 if ($parsedAvailablePeriod) {
371 $lastAvailablePeriod = $parsedAvailablePeriod[sizeof($parsedAvailablePeriod) - 1];
372
373 if ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
374 // unavailable interval is inside available interval
375 $fixedPeriod = array_pop($parsedAvailablePeriod);
376
377 if ($fixedPeriod[0] !== $unavailable[0]) {
378 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
379 }
380
381 if ($unavailable[1] !== $fixedPeriod[1]) {
382 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
383 }
384 } elseif ($unavailable[0] <= $lastAvailablePeriod[0] && $unavailable[1] >= $lastAvailablePeriod[1]) {
385 // available interval is inside unavailable interval
386 array_pop($parsedAvailablePeriod);
387 } elseif ($unavailable[0] <= $lastAvailablePeriod[0] && $unavailable[1] >= $lastAvailablePeriod[0] && $unavailable[1] <= $lastAvailablePeriod[1]) {
388 // unavailable interval intersect start of available interval
389 $fixedPeriod = array_pop($parsedAvailablePeriod);
390
391 if ($unavailable[1] !== $fixedPeriod[1]) {
392 $parsedAvailablePeriod[] = [$unavailable[1], $fixedPeriod[1], $fixedPeriod[2]];
393 }
394 } elseif ($unavailable[0] >= $lastAvailablePeriod[0] && $unavailable[0] <= $lastAvailablePeriod[1] && $unavailable[1] >= $lastAvailablePeriod[1]) {
395 // unavailable interval intersect end of available interval
396 $fixedPeriod = array_pop($parsedAvailablePeriod);
397
398 if ($fixedPeriod[0] !== $unavailable[0]) {
399 $parsedAvailablePeriod[] = [$fixedPeriod[0], $unavailable[0], $fixedPeriod[2]];
400 }
401 }
402 }
403 }
404 }
405
406 return $parsedAvailablePeriod;
407 }
408
409 /**
410 * @param Service $service
411 * @param Provider $provider
412 * @param int $personsCount
413 *
414 * @return bool
415 *
416 * @throws Exception
417 */
418 private function getOnlyAppointmentsSlots($service, $provider, $personsCount)
419 {
420 $getOnlyAppointmentsSlots = false;
421
422 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
423 /** @var Service $providerService */
424 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
425
426 if ($personsCount < $providerService->getMinCapacity()->getValue()) {
427 $getOnlyAppointmentsSlots = true;
428 }
429 }
430
431 return $getOnlyAppointmentsSlots;
432 }
433
434 /** @noinspection MoreThanThreeArgumentsInspection */
435 /**
436 * @param Service $service
437 * @param int $locationId
438 * @param Collection $providers
439 * @param Collection $locations
440 * @param array $globalDaysOffDates
441 * @param DateTime $startDateTime
442 * @param DateTime $endDateTime
443 * @param int $personsCount
444 * @param boolean $bookIfPending
445 * @param boolean $bookIfNotMin
446 * @param boolean $bookAfterMin
447 * @param boolean $bookOverApp
448 * @param array $appointmentsCount
449 *
450 * @return array
451 * @throws Exception
452 */
453 private function getFreeTime(
454 Service $service,
455 $locationId,
456 Collection $locations,
457 Collection $providers,
458 array $globalDaysOffDates,
459 DateTime $startDateTime,
460 DateTime $endDateTime,
461 $personsCount,
462 $bookIfPending,
463 $bookIfNotMin,
464 $bookAfterMin,
465 $bookOverApp,
466 $appointmentsCount
467 ) {
468
469 $weekDayIntervals = [];
470
471 $appointmentIntervals = [];
472
473 $daysOffDates = [];
474
475 $specialDayIntervals = [];
476
477 $getOnlyAppointmentsSlots = [];
478
479 $serviceId = $service->getId()->getValue();
480
481 /** @var Provider $provider */
482 foreach ($providers->getItems() as $provider) {
483 $providerId = $provider->getId()->getValue();
484
485 $getOnlyAppointmentsSlots[$providerId] = $bookIfNotMin && $bookAfterMin ? $this->getOnlyAppointmentsSlots(
486 $service,
487 $provider,
488 $personsCount
489 ) : false;
490
491 $daysOffDates[$providerId] = $this->getProviderDayOffDates($provider);
492
493 $weekDayIntervals[$providerId] = $this->scheduleService->getProviderWeekDaysIntervals(
494 $provider,
495 $locations,
496 $locationId,
497 $serviceId
498 );
499
500 $specialDayIntervals[$providerId] = $this->scheduleService->getProviderSpecialDayIntervals(
501 $provider,
502 $locations,
503 $locationId,
504 $serviceId
505 );
506
507 $appointmentIntervals[$providerId] = $this->getProviderAppointmentIntervals(
508 $provider,
509 $locations,
510 $serviceId,
511 $locationId,
512 $personsCount,
513 $bookIfPending,
514 $bookOverApp,
515 $weekDayIntervals[$providerId],
516 $specialDayIntervals[$providerId]
517 );
518 }
519
520 $freeDateIntervals = [];
521
522 foreach ($appointmentIntervals as $providerKey => $providerDates) {
523 foreach ((array)$providerDates as $dateKey => $dateIntervals) {
524 $dayIndex = DateTimeService::getDayIndex($dateKey);
525
526 $specialDayDateKey = null;
527
528 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
529 if (array_key_exists($dateKey, $specialDays['dates'])) {
530 $specialDayDateKey = $specialDayKey;
531 break;
532 }
533 }
534
535 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
536 // get free intervals if it is special day
537 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
538 $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
539 !empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []
540 );
541 } elseif (isset($weekDayIntervals[$providerKey][$dayIndex]['free']) && !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])) {
542 // get free intervals if it is working day
543 $unavailableIntervals =
544 $weekDayIntervals[$providerKey][$dayIndex]['busy'] + (!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : []);
545
546 $intersectedTimes = array_intersect(
547 array_keys($weekDayIntervals[$providerKey][$dayIndex]['busy']),
548 array_keys(!empty($dateIntervals['occupied']) ? $dateIntervals['occupied'] : [])
549 );
550
551 foreach ($intersectedTimes as $time) {
552 $unavailableIntervals[$time] =
553 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] >
554 $dateIntervals['occupied'][$time] ?
555 $weekDayIntervals[$providerKey][$dayIndex]['busy'][$time] :
556 $dateIntervals['occupied'][$time];
557 }
558
559 $freeDateIntervals[$providerKey][$dateKey] = $this->getAvailableIntervals(
560 $weekDayIntervals[$providerKey][$dayIndex]['free'],
561 $unavailableIntervals ?: []
562 );
563 }
564 }
565 }
566
567 $startDateTime = clone $startDateTime;
568
569 $startDateTime->setTime(0, 0);
570
571 $endDateTime = clone $endDateTime;
572
573 $endDateTime->modify('+1 day')->setTime(0, 0);
574
575 // create calendar
576 $period = new DatePeriod(
577 $startDateTime,
578 new DateInterval('P1D'),
579 $endDateTime
580 );
581
582 $calendar = [];
583
584 /** @var DateTime $day */
585 foreach ($period as $day) {
586 $currentDate = $day->format('Y-m-d');
587 $dayIndex = (int)$day->format('N');
588
589 $isGlobalDayOff = array_key_exists($currentDate, $globalDaysOffDates) ||
590 array_key_exists($day->format('m-d'), $globalDaysOffDates);
591
592 if (!$isGlobalDayOff) {
593 foreach ($weekDayIntervals as $providerKey => $providerWorkingHours) {
594 $isProviderDayOff = array_key_exists($currentDate, $daysOffDates[$providerKey]) ||
595 array_key_exists($day->format('m-d'), $daysOffDates[$providerKey]);
596
597 $specialDayDateKey = null;
598
599 foreach ((array)$specialDayIntervals[$providerKey] as $specialDayKey => $specialDays) {
600 if (array_key_exists($currentDate, $specialDays['dates'])) {
601 $specialDayDateKey = $specialDayKey;
602 break;
603 }
604 }
605
606 if (!$isProviderDayOff) {
607 if (!empty($appointmentsCount['limitCount']) && !empty($appointmentsCount['appCount'][$providerKey][$currentDate]) &&
608 $appointmentsCount['appCount'][$providerKey][$currentDate] >= $appointmentsCount['limitCount']) {
609 continue;
610 }
611
612 if ($freeDateIntervals && isset($freeDateIntervals[$providerKey][$currentDate])) {
613 // get date intervals if there are appointments (special or working day)
614 $calendar[$currentDate][$providerKey] = [
615 'slots' => $personsCount && $bookIfNotMin && isset($appointmentIntervals[$providerKey][$currentDate]['available']) ?
616 $appointmentIntervals[$providerKey][$currentDate]['available'] : [],
617 'full' => isset($appointmentIntervals[$providerKey][$currentDate]['full']) ?
618 $appointmentIntervals[$providerKey][$currentDate]['full'] : [],
619 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $freeDateIntervals[$providerKey][$currentDate],
620 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
621 ];
622 } else {
623 if ($specialDayDateKey !== null && isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'])) {
624 // get date intervals if it is special day with out appointments
625 $calendar[$currentDate][$providerKey] = [
626 'slots' => [],
627 'full' => [],
628 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $specialDayIntervals[$providerKey][$specialDayDateKey]['intervals']['free'],
629 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
630 ];
631 } elseif (isset($weekDayIntervals[$providerKey][$dayIndex]) && !isset($specialDayIntervals[$providerKey][$specialDayDateKey]['intervals'])) {
632 // get date intervals if it is working day without appointments
633 $calendar[$currentDate][$providerKey] = [
634 'slots' => [],
635 'full' => [],
636 'intervals' => $getOnlyAppointmentsSlots[$providerKey] ? [] : $weekDayIntervals[$providerKey][$dayIndex]['free'],
637 'count' => !empty($appointmentsCount[$providerKey][$currentDate]) ? $appointmentsCount[$providerKey][$currentDate] : 0
638 ];
639 }
640 }
641 }
642 }
643 }
644 }
645
646 return $calendar;
647 }
648
649 /** @noinspection MoreThanThreeArgumentsInspection */
650 /**
651 * @param Service $service
652 * @param int $requiredTime
653 * @param array $freeIntervals
654 * @param array $resourcedIntervals
655 * @param int $slotLength
656 * @param DateTime $startDateTime
657 * @param bool $serviceDurationAsSlot
658 * @param bool $bufferTimeInSlot
659 * @param bool $isFrontEndBooking
660 * @param String $timeZone
661 *
662 * @return array
663 */
664 private function getAppointmentFreeSlots(
665 $service,
666 $requiredTime,
667 &$freeIntervals,
668 $resourcedIntervals,
669 $slotLength,
670 $startDateTime,
671 $serviceDurationAsSlot,
672 $bufferTimeInSlot,
673 $isFrontEndBooking,
674 $timeZone
675 ) {
676 $availableResult = [];
677
678 $occupiedResult = [];
679
680 $realRequiredTime = $requiredTime -
681 $service->getTimeBefore()->getValue() -
682 $service->getTimeAfter()->getValue();
683
684 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
685 $requiredTime = $requiredTime -
686 $service->getTimeBefore()->getValue() -
687 $service->getTimeAfter()->getValue();
688 }
689
690 $currentDateTime = DateTimeService::getNowDateTimeObject();
691
692 $currentDateString = $currentDateTime->format('Y-m-d');
693
694 $currentTimeStringInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
695
696 $currentTimeInSeconds = $this->intervalService->getSeconds($currentDateTime->format('H:i:s'));
697
698 $currentDateFormatted = $currentDateTime->format('Y-m-d');
699
700 $startTimeInSeconds = $this->intervalService->getSeconds($startDateTime->format('H:i:s'));
701
702 $startDateFormatted = $startDateTime->format('Y-m-d');
703
704 $bookingLength = $serviceDurationAsSlot && $isFrontEndBooking ? $requiredTime : $slotLength;
705
706 $appCount = [];
707
708 $isContinuousTime = false;
709
710 $continuousTimeSlot = null;
711
712 foreach ($freeIntervals as $dateKey => $dateProviders) {
713 foreach ((array)$dateProviders as $providerKey => $provider) {
714 foreach ((array)$provider['intervals'] as $timePeriod) {
715 $moveStart = false;
716
717 if ($timePeriod[0] === 0 && $isContinuousTime && $continuousTimeSlot !== null) {
718 $isContinuousTime = false;
719
720 $moveStart = true;
721 }
722
723 if ($timePeriod[1] === 86400) {
724 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
725 $dateKey . ' 00:00:00',
726 $timeZone
727 )->modify('+1 days')->format('Y-m-d');
728
729 if (isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) &&
730 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] === 0
731 ) {
732 $isContinuousTime = true;
733
734 $nextDayInterval = $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1];
735
736 $timePeriod[1] += (
737 $realRequiredTime + $service->getTimeAfter()->getValue() <= $nextDayInterval
738 ? $realRequiredTime + $service->getTimeAfter()->getValue()
739 : $nextDayInterval);
740 }
741 }
742
743 if ($serviceDurationAsSlot && !$bufferTimeInSlot) {
744 $timePeriod[1] = $timePeriod[1] - $service->getTimeAfter()->getValue();
745 }
746
747 $customerTimeStart = $timePeriod[0] + (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
748
749 $providerTimeStart = $customerTimeStart - (!$moveStart ? $service->getTimeBefore()->getValue() : 0);
750
751 $numberOfSlots = (int)(
752 floor(
753 (
754 $timePeriod[1] -
755 $providerTimeStart -
756 ($requiredTime - ($moveStart ? $service->getTimeBefore()->getValue() : 0))
757 ) / $bookingLength
758 ) + 1
759 );
760
761 $inspectResourceIndexes = [];
762
763 if (isset($resourcedIntervals[$dateKey])) {
764 foreach ($resourcedIntervals[$dateKey] as $resourceIndex => $resourceData) {
765 if (array_intersect(
766 $timePeriod[2],
767 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
768 )) {
769 $inspectResourceIndexes[] = $resourceIndex;
770 }
771 }
772 }
773
774 $providerPeriodSlots = [];
775
776 $achievedLength = 0;
777
778 if ($moveStart && $continuousTimeSlot !== 86400 && ($bookingLength - (86400 - $continuousTimeSlot)) >= 0) {
779 $customerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
780
781 $providerTimeStart += $bookingLength - (86400 - $continuousTimeSlot);
782
783 $numberOfSlots = (int)(floor(($timePeriod[1] - $providerTimeStart - $requiredTime) / $bookingLength) + 1);
784 }
785
786 if ($moveStart) {
787 $continuousTimeSlot = null;
788 }
789
790 for ($i = 0; $i < $numberOfSlots; $i++) {
791 $achievedLength += $bookingLength;
792
793 $timeSlot = $customerTimeStart + $i * $bookingLength;
794
795 if (($startDateFormatted !== $dateKey && ($serviceDurationAsSlot && !$bufferTimeInSlot ? $timeSlot <= $timePeriod[1] - $requiredTime : true)) ||
796 ($startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot) ||
797 ($startDateFormatted === $currentDateFormatted && $startDateFormatted === $dateKey && $startTimeInSeconds < $timeSlot && $currentTimeInSeconds < $timeSlot)
798 ) {
799 $timeSlotEnd = $timeSlot + $bookingLength;
800
801 $filteredLocationsIds = $timePeriod[2];
802
803 foreach ($inspectResourceIndexes as $resourceIndex) {
804 foreach ($resourcedIntervals[$dateKey][$resourceIndex]['intervals'] as $start => $end) {
805 if (($start >= $timeSlot && $start < $timeSlotEnd) ||
806 ($end > $timeSlot && $end <= $timeSlotEnd) ||
807 ($start <= $timeSlot && $end >= $timeSlotEnd) ||
808 ($start >= $timeSlot && $start < $timeSlot + $requiredTime)
809 ) {
810 $filteredLocationsIds = array_diff(
811 $filteredLocationsIds,
812 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds']
813 );
814
815 if (!$filteredLocationsIds) {
816 if ($achievedLength < $requiredTime) {
817 $providerPeriodSlots = [];
818
819 $achievedLength = 0;
820 }
821
822 continue 3;
823 }
824
825 $removedLocationsIds = array_diff(
826 $resourcedIntervals[$dateKey][$resourceIndex]['locationsIds'],
827 $filteredLocationsIds
828 );
829
830 if ($removedLocationsIds && $achievedLength < $requiredTime) {
831 $parsedPeriodSlots = [];
832
833 foreach ($providerPeriodSlots as $previousTimeSlot => $periodSlotData) {
834 if ($start >= $previousTimeSlot &&
835 $start < $previousTimeSlot + $requiredTime
836 ) {
837 foreach ($periodSlotData as $data) {
838 if (!in_array($data[1], $removedLocationsIds)) {
839 $parsedPeriodSlots[$previousTimeSlot][] = $data;
840 }
841 }
842 } else {
843 $parsedPeriodSlots[$previousTimeSlot] = $periodSlotData;
844 }
845 }
846
847 $providerPeriodSlots = $parsedPeriodSlots;
848 }
849 }
850 }
851 }
852
853 if (!$timePeriod[2]) {
854 $providerPeriodSlots[$timeSlot][] = [$providerKey, null];
855 } else if ($filteredLocationsIds) {
856 foreach ($filteredLocationsIds as $locationId) {
857 $providerPeriodSlots[$timeSlot][] = [$providerKey, $locationId];
858 }
859 }
860 }
861 }
862
863 foreach ($providerPeriodSlots as $timeSlot => $data) {
864 $time = sprintf('%02d', floor($timeSlot / 3600)) . ':'
865 . sprintf('%02d', floor(($timeSlot / 60) % 60));
866
867 if ($time !== '24:00') {
868 $availableResult[$dateKey][$time] = $data;
869
870 if ($isContinuousTime) {
871 $continuousTimeSlot = $timeSlot;
872 }
873 }
874 }
875 }
876
877 foreach ($provider['slots'] as $appointmentTime => $appointmentData) {
878 $startInSeconds = $this->intervalService->getSeconds($appointmentTime . ':00');
879
880 if ($currentDateString === $dateKey &&
881 ($currentTimeStringInSeconds > $startInSeconds || $startTimeInSeconds > $startInSeconds)
882 ) {
883 continue;
884 }
885
886 $endInSeconds = $this->intervalService->getSeconds($appointmentData['endTime']) + $service->getTimeAfter()->getValue();
887
888 $newEndInSeconds = $startInSeconds + $realRequiredTime;
889
890 if ($newEndInSeconds !== 86400 &&
891 ($newEndInSeconds > 86400 ? $newEndInSeconds - 86400 > $endInSeconds : $newEndInSeconds > $endInSeconds)
892 ) {
893 if ($dateKey !== $appointmentData['endDate']) {
894 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
895 $dateKey . ' 00:00:00',
896 $timeZone
897 )->modify('+1 days')->format('Y-m-d');
898
899 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
900 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != $endInSeconds ||
901 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
902 ) {
903 continue;
904 }
905 } elseif ($newEndInSeconds > 86400) {
906 $nextIntervalIsValid = false;
907
908 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
909 if ($interval[0] === $endInSeconds && $interval[1] === 86400) {
910 $nextIntervalIsValid = true;
911
912 break;
913 }
914 }
915
916 if (!$nextIntervalIsValid) {
917 continue;
918 }
919
920 $nextDateString = DateTimeService::getDateTimeObjectInTimeZone(
921 $dateKey . ' 00:00:00',
922 $timeZone
923 )->modify('+1 days')->format('Y-m-d');
924
925 if (!isset($freeIntervals[$nextDateString][$providerKey]['intervals'][0]) ||
926 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][0] != 0 ||
927 $freeIntervals[$nextDateString][$providerKey]['intervals'][0][1] < $newEndInSeconds - 86400
928 ) {
929 continue;
930 }
931 } else {
932 $nextIntervalIsValid = false;
933
934 foreach ($freeIntervals[$dateKey][$providerKey]['intervals'] as $interval) {
935 if ($interval[0] === $endInSeconds && $interval[1] >= $newEndInSeconds) {
936 $nextIntervalIsValid = true;
937
938 break;
939 }
940 }
941
942 if (!$nextIntervalIsValid) {
943 continue;
944 }
945 }
946 }
947
948 $availableResult[$dateKey][$appointmentTime] = [
949 [
950 $providerKey,
951 $appointmentData['locationId'],
952 $appointmentData['places'],
953 $appointmentData['serviceId'],
954 $appointmentData['duration'],
955 ]
956 ];
957 }
958
959 foreach ($provider['full'] as $appointmentTime => $appointmentData) {
960 $occupiedResult[$dateKey][$appointmentTime][] = [
961 $providerKey,
962 $appointmentData['locationId'],
963 $appointmentData['places'],
964 $appointmentData['serviceId'],
965 $appointmentData['duration'],
966 ];
967 }
968
969 $appCount[$dateKey] = $freeIntervals[$dateKey][$providerKey]['count'];
970 }
971 }
972
973 return [
974 'available' => $availableResult,
975 'occupied' => $occupiedResult,
976 'appCount' => $appCount
977 ];
978 }
979
980 /**
981 * @param array $slots
982 * @param string $timeZone
983 *
984 * @return array
985 * @throws Exception
986 */
987 private function getSlotsInMainTimeZoneFromTimeZone($slots, $timeZone)
988 {
989 $convertedProviderSlots = [];
990
991 foreach ($slots as $slotDate => $slotTimes) {
992 foreach ($slots[$slotDate] as $slotTime => $slotTimesProviders) {
993 $convertedSlotParts = explode(
994 ' ',
995 DateTimeService::getDateTimeObjectInTimeZone(
996 $slotDate . ' ' . $slotTime,
997 $timeZone
998 )->setTimezone(new DateTimeZone(DateTimeService::getTimeZone()->getName()))->format('Y-m-d H:i')
999 );
1000
1001 $convertedProviderSlots[$convertedSlotParts[0]][$convertedSlotParts[1]] = $slotTimesProviders;
1002 }
1003 }
1004
1005 return $convertedProviderSlots;
1006 }
1007
1008
1009 /**
1010 * @param Collection $appointments
1011 * @param int $excludeAppointmentId
1012 *
1013 * @return array
1014 * @throws Exception
1015 */
1016 public function getAppointmentCount($appointments, $excludeAppointmentId)
1017 {
1018 $appCount = [];
1019
1020 /** @var Appointment $appointment */
1021 foreach ($appointments->getItems() as $appointment) {
1022 if (!$excludeAppointmentId || empty($appointment->getId()) || $appointment->getId()->getValue() !== $excludeAppointmentId) {
1023 if (!empty($appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')])) {
1024 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')]++;
1025 } else {
1026 $appCount[$appointment->getProviderId()->getValue()][$appointment->getBookingStart()->getValue()->format('Y-m-d')] = 1;
1027 }
1028 }
1029 }
1030
1031 return $appCount;
1032 }
1033
1034 /** @noinspection MoreThanThreeArgumentsInspection */
1035 /**
1036 * @param array $settings
1037 * @param array $props
1038 * @param SlotsEntities $slotsEntities
1039 * @param Collection $appointments
1040 *
1041 * @return array
1042 * @throws Exception
1043 */
1044 public function getSlots($settings, $props, $slotsEntities, $appointments)
1045 {
1046 $appointmentsCount = $this->getAppointmentCount($appointments, $props['excludeAppointmentId']);
1047
1048 $resourcedLocationsIntervals = $slotsEntities->getResources()->length() ?
1049 $this->resourceService->manageResources(
1050 $slotsEntities->getResources(),
1051 $appointments,
1052 $slotsEntities->getLocations(),
1053 $slotsEntities->getServices()->getItem($props['serviceId']),
1054 $slotsEntities->getProviders(),
1055 $props['locationId'],
1056 $props['excludeAppointmentId'],
1057 array_key_exists('totalPersons', $props) ? $props['totalPersons'] : $props['personsCount']
1058 ) : [];
1059
1060 $continuousAppointments = $this->entityService->filterSlotsAppointments($slotsEntities, $appointments, $props);
1061
1062 $this->providerService->addAppointmentsToAppointmentList(
1063 $slotsEntities->getProviders(),
1064 $appointments,
1065 $settings['isGloballyBusySlot']
1066 );
1067
1068 return $this->getCalculatedFreeSlots(
1069 $settings,
1070 $props,
1071 $slotsEntities,
1072 $resourcedLocationsIntervals,
1073 $continuousAppointments,
1074 $appointmentsCount
1075 );
1076 }
1077
1078 /** @noinspection MoreThanThreeArgumentsInspection */
1079 /**
1080 * @param array $settings
1081 * @param array $props
1082 * @param SlotsEntities $slotsEntities
1083 * @param array $resourcedLocationsIntervals
1084 * @param array $continuousAppointments
1085 * @param array $appointmentsCount
1086 *
1087 * @return array
1088 * @throws Exception
1089 */
1090 private function getCalculatedFreeSlots(
1091 $settings,
1092 $props,
1093 $slotsEntities,
1094 $resourcedLocationsIntervals,
1095 $continuousAppointments,
1096 $appointmentsCount
1097 ) {
1098 $freeProvidersSlots = [];
1099
1100 /** @var DateTime $startDateTime */
1101 $startDateTime = $props['startDateTime'];
1102
1103 /** @var DateTime $endDateTime */
1104 $endDateTime = $props['endDateTime'];
1105
1106 /** @var Service $service */
1107 $service = $slotsEntities->getServices()->getItem($props['serviceId']);
1108
1109 /** @var Collection $providers */
1110 $providers = $slotsEntities->getProviders();
1111
1112 /** @var Collection $locations */
1113 $locations = $slotsEntities->getLocations();
1114
1115 $requiredTime = $this->entityService->getAppointmentRequiredTime(
1116 $service,
1117 $props['extras']
1118 );
1119
1120 /** @var Provider $provider */
1121 foreach ($providers->getItems() as $provider) {
1122 if ($provider->getServiceList()->keyExists($service->getId()->getValue())) {
1123 /** @var Service $providerService */
1124 $providerService = $provider->getServiceList()->getItem($service->getId()->getValue());
1125
1126 if ($providerService && $props['personsCount'] > $providerService->getMaxCapacity()->getValue()) {
1127 continue;
1128 }
1129 }
1130
1131 $providerContainer = new Collection();
1132
1133 if ($provider->getTimeZone()) {
1134 $this->providerService->modifyProviderTimeZone(
1135 $provider,
1136 $settings['allowAdminBookAtAnyTime'] ? [] : $settings['globalDaysOff'],
1137 $startDateTime,
1138 $endDateTime
1139 );
1140 }
1141
1142 $start = $provider->getTimeZone() ?
1143 DateTimeService::getCustomDateTimeObjectInTimeZone(
1144 $startDateTime->format('Y-m-d H:i'),
1145 $provider->getTimeZone()->getValue()
1146 ) : DateTimeService::getCustomDateTimeObject($startDateTime->format('Y-m-d H:i'));
1147
1148 $end = $provider->getTimeZone() ?
1149 DateTimeService::getCustomDateTimeObjectInTimeZone(
1150 $endDateTime->format('Y-m-d H:i'),
1151 $provider->getTimeZone()->getValue()
1152 ) : DateTimeService::getCustomDateTimeObject($endDateTime->format('Y-m-d H:i'));
1153
1154 $providerContainer->addItem($provider, $provider->getId()->getValue());
1155
1156 $limitPerEmployee = !empty($settings['limitPerEmployee']) && !empty($settings['limitPerEmployee']['enabled']) ?
1157 $settings['limitPerEmployee']['numberOfApp'] : null;
1158
1159 $freeIntervals = $this->getFreeTime(
1160 $service,
1161 $props['locationId'],
1162 $locations,
1163 $providerContainer,
1164 $settings['allowAdminBookAtAnyTime'] || $provider->getTimeZone() ?
1165 [] : $settings['globalDaysOff'],
1166 $start,
1167 $end,
1168 $props['personsCount'],
1169 $props['isFrontEndBooking'] && $settings['allowBookingIfPending'] && $settings['defaultAppointmentStatus'] === BookingStatus::PENDING,
1170 $settings['allowBookingIfNotMin'],
1171 $props['isFrontEndBooking'] ? $settings['openedBookingAfterMin'] : false,
1172 !empty($settings['allowAdminBookOverApp']),
1173 ['limitCount' => $limitPerEmployee, 'appCount' => $appointmentsCount]
1174 );
1175
1176 $freeProvidersSlots[$provider->getId()->getValue()] = $this->getAppointmentFreeSlots(
1177 $service,
1178 $requiredTime,
1179 $freeIntervals,
1180 !empty($resourcedLocationsIntervals[$provider->getId()->getValue()])
1181 ? $resourcedLocationsIntervals[$provider->getId()->getValue()] : [],
1182 $settings['timeSlotLength'] ?: $requiredTime,
1183 $start,
1184 $settings['allowAdminBookAtAnyTime'] ? $settings['adminServiceDurationAsSlot'] :
1185 $settings['serviceDurationAsSlot'],
1186 $settings['bufferTimeInSlot'],
1187 true,
1188 $provider->getTimeZone() ?
1189 $provider->getTimeZone()->getValue() : DateTimeService::getTimeZone()->getName()
1190 );
1191 }
1192
1193 $freeSlots = [
1194 'available' => [],
1195 'occupied' => [],
1196 'continuousAppointments' => $continuousAppointments[0],
1197 'appCount' => [],
1198 'duration' => $requiredTime / 60,
1199 ];
1200
1201 foreach ($freeProvidersSlots as $providerKey => $providerSlots) {
1202 /** @var Provider $provider */
1203 $provider = $providers->getItem($providerKey);
1204
1205 $freeSlots['appCount'][$providerKey] = $providerSlots['appCount'];
1206
1207 foreach (['available', 'occupied'] as $type) {
1208 if ($provider->getTimeZone()) {
1209 $providerSlots[$type] = $this->getSlotsInMainTimeZoneFromTimeZone(
1210 $providerSlots[$type],
1211 $provider->getTimeZone()->getValue()
1212 );
1213 }
1214
1215 foreach ($providerSlots[$type] as $dateKey => $dateSlots) {
1216 foreach ($dateSlots as $timeKey => $slotData) {
1217 if (empty($freeSlots[$type][$dateKey][$timeKey])) {
1218 $freeSlots[$type][$dateKey][$timeKey] = [];
1219 }
1220
1221 foreach ($slotData as $item) {
1222 $freeSlots[$type][$dateKey][$timeKey][] = $item;
1223 }
1224
1225 if (isset($freeSlots[$type][$dateKey])) {
1226 if (!$freeSlots[$type][$dateKey]) {
1227 unset($freeSlots[$type][$dateKey]);
1228 } else {
1229 ksort($freeSlots[$type][$dateKey]);
1230 }
1231 }
1232 }
1233 }
1234 }
1235 }
1236
1237 return $freeSlots;
1238 }
1239 }
1240