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