PluginProbe ʕ •ᴥ•ʔ
Booking for Appointments and Events Calendar – Amelia / 2.0.2
Booking for Appointments and Events Calendar – Amelia v2.0.2
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 / Application / Commands / Calendar / GetCalendarSlotsCommandHandler.php
ameliabooking / src / Application / Commands / Calendar Last commit date
CalendarRescheduleEventCommand.php 6 months ago CalendarRescheduleEventCommandHandler.php 6 months ago GetCalendarEventsCommand.php 6 months ago GetCalendarEventsCommandHandler.php 6 months ago GetCalendarSlotAvailabilityCommand.php 6 months ago GetCalendarSlotAvailabilityHandler.php 6 months ago GetCalendarSlotEntitiesCommand.php 6 months ago GetCalendarSlotEntitiesCommandHandler.php 6 months ago GetCalendarSlotsCommand.php 6 months ago GetCalendarSlotsCommandHandler.php 6 months ago
GetCalendarSlotsCommandHandler.php
519 lines
1 <?php
2
3 /**
4 * @copyright © Melograno Ventures. All rights reserved.
5 * @licence See LICENCE.md for license details.
6 */
7
8 namespace AmeliaBooking\Application\Commands\Calendar;
9
10 use AmeliaBooking\Application\Commands\CommandHandler;
11 use AmeliaBooking\Application\Commands\CommandResult;
12 use AmeliaBooking\Domain\Entity\Schedule\DayOff;
13 use AmeliaBooking\Domain\Entity\Schedule\SpecialDay;
14 use AmeliaBooking\Domain\Entity\Schedule\WeekDay;
15 use AmeliaBooking\Domain\Entity\User\Provider;
16 use AmeliaBooking\Domain\Services\DateTime\DateTimeService;
17 use AmeliaBooking\Domain\ValueObjects\String\BookingStatus;
18 use AmeliaBooking\Domain\ValueObjects\String\Status;
19 use DateInterval;
20 use DatePeriod;
21 use DateTime;
22 use DateTimeZone;
23
24 class GetCalendarSlotsCommandHandler extends CommandHandler
25 {
26 private $timeLimits = ['slotMinTime' => '24:00:00', 'slotMaxTime' => '00:00:00'];
27
28 public function handle(GetCalendarSlotsCommand $command): CommandResult
29 {
30 $result = new CommandResult();
31
32 $providerRepository = $this->container->get('domain.users.providers.repository');
33 $locationRepository = $this->container->get('domain.locations.repository');
34
35 $queryParams = $command->getField('queryParams');
36 $allWorkDays = [];
37 $selectedService = $queryParams['service'] ?? null;
38
39 $queryParams['locations'] = array_map(
40 fn($location) => $location['id'],
41 $locationRepository->getFiltered(
42 ['status' => !empty($queryParams['providers']) ? null : Status::VISIBLE],
43 0
44 )->toArray()
45 );
46
47 $criteria = ['providerStatus' => !empty($queryParams['providers']) ? null : Status::VISIBLE];
48 foreach ($queryParams as $key => $value) {
49 if ($key !== 'providerStatus') {
50 $criteria[$key] = $value;
51 }
52 }
53
54 $providers = $providerRepository->getWithSchedule($criteria)->getItems();
55
56 foreach ($providers as $provider) {
57 if (!$selectedService) {
58 $providerWorkDays = $this->getProviderWorkDays($provider, $queryParams);
59 $this->getTimeLimitsByProvider($queryParams, $providerWorkDays, $provider);
60 $this->mergeProviderWorkDays($allWorkDays, $providerWorkDays);
61 }
62 }
63
64 if (empty($allWorkDays)) {
65 $this->fillEmptyWorkDays($allWorkDays, $queryParams);
66 }
67
68 $this->processCompanyDaysOff($allWorkDays, $queryParams);
69 $formattedWorkPeriods = $this->formatWorkDays($allWorkDays);
70
71 $this->getTimeLimitsFromAppointmentsAndEvents($queryParams);
72
73 $result->setData([
74 'workPeriods' => $formattedWorkPeriods,
75 'slotMinTime' => $this->timeLimits['slotMinTime'],
76 'slotMaxTime' => $this->timeLimits['slotMaxTime'],
77 'now' => DateTimeService::getNowDateTime()
78 ]);
79
80 return $result;
81 }
82
83 private function fillEmptyWorkDays(array &$allWorkDays, array $queryParams): void
84 {
85 [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = $this->getLimitsFromCompanyWorkHours();
86
87 if ($this->timeLimits['slotMinTime'] === '24:00:00' && $this->timeLimits['slotMaxTime'] === '00:00:00') {
88 $this->timeLimits['slotMinTime'] = '09:00:00';
89 $this->timeLimits['slotMaxTime'] = '17:00:00';
90 }
91
92 $calendarStartDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarStartDate']);
93 $calendarEndDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarEndDate']);
94
95 $datePeriod = new DatePeriod($calendarStartDate, new DateInterval('P1D'), $calendarEndDate);
96
97 foreach ($datePeriod as $date) {
98 $dateString = $date->format('Y-m-d');
99 $allWorkDays[$dateString] = ['groupId' => 'notWorkHours', 'periods' => []];
100 }
101 }
102
103 private function getProviderWorkDays(Provider $provider, array $queryParams): array
104 {
105 $providerTimeZone = $provider->getTimezone() ? $provider->getTimezone()->getValue() : DateTimeService::getTimeZone()->getName();
106 $currentUserTimeZone = DateTimeService::getTimeZone();
107
108 $employeeDays = [];
109 $startDate = new DateTime($queryParams['calendarStartDate'], $currentUserTimeZone);
110 $endDate = new DateTime($queryParams['calendarEndDate'], $currentUserTimeZone);
111
112 $datePeriod = new DatePeriod($startDate, new DateInterval('P1D'), $endDate);
113
114 $weekDays = $provider->getWeekDayList()->getItems();
115 $specialDays = $provider->getSpecialDayList()->getItems();
116 $daysOff = $provider->getDayOffList()->getItems();
117
118 foreach ($datePeriod as $date) {
119 $dateString = $date->format('Y-m-d');
120 $weekDay = $this->findMatchingDay($weekDays, $date->format('N'));
121
122 $this->mapPeriods(
123 $employeeDays,
124 $weekDay ? $weekDay->getPeriodList()->getItems() : [],
125 $dateString,
126 $providerTimeZone,
127 $currentUserTimeZone,
128 'workHours'
129 );
130
131 $specialDay = $this->findMatchingSpecialDay($specialDays, $date);
132
133 if ($specialDay) {
134 $this->mapPeriods(
135 $employeeDays,
136 $specialDay->getPeriodList()->getItems(),
137 $dateString,
138 $providerTimeZone,
139 $currentUserTimeZone,
140 'workHours specialDay'
141 );
142 }
143
144 $dayOff = $this->findMatchingDayOff($daysOff, $date);
145 if ($dayOff) {
146 $employeeDays[$dateString] = [
147 'groupId' => 'dayOff',
148 'periods' => []
149 ];
150 }
151 }
152
153 return $employeeDays;
154 }
155
156 private function findMatchingDay(array $weekDays, int $dateDayIndex): ?WeekDay
157 {
158 foreach ($weekDays as $weekDay) {
159 if ($weekDay->getDayIndex()->getValue() === $dateDayIndex) {
160 return $weekDay;
161 }
162 }
163 return null;
164 }
165
166 private function findMatchingSpecialDay(array $specialDays, DateTime $date): ?SpecialDay
167 {
168 foreach ($specialDays as $specialDay) {
169 if ($date >= $specialDay->getStartDate()->getValue() && $date <= $specialDay->getEndDate()->getValue()) {
170 return $specialDay;
171 }
172 }
173 return null;
174 }
175
176 private function findMatchingDayOff(array $daysOff, DateTime $date): ?DayOff
177 {
178 foreach ($daysOff as $dayOff) {
179 if ($date >= $dayOff->getStartDate()->getValue() && $date <= $dayOff->getEndDate()->getValue()) {
180 return $dayOff;
181 }
182 }
183 return null;
184 }
185
186 private function mapPeriods(
187 array &$employeeDays,
188 array $periods,
189 string $dateString,
190 string $providerTimeZone,
191 DateTimeZone $currentUserTimeZone,
192 string $groupId = 'workHours'
193 ): void {
194 if (!isset($employeeDays[$dateString])) {
195 $employeeDays[$dateString] = [
196 'groupId' => 'workHours',
197 'periods' => []
198 ];
199 }
200
201 foreach ($periods as $period) {
202 $startDateTime = $this->convertWorkPeriods(
203 new DateTime($dateString . $period->getStartTime()->getValue()->format('H:i:s')),
204 $providerTimeZone,
205 $currentUserTimeZone
206 );
207
208 $endDateTime = $this->convertWorkPeriods(
209 new DateTime($dateString . $period->getEndTime()->getValue()->format('H:i:s')),
210 $providerTimeZone,
211 $currentUserTimeZone
212 );
213
214 $startDate = $startDateTime->format('Y-m-d');
215 $startTime = $startDateTime->format('H:i:s');
216 $endDate = $endDateTime->format('Y-m-d');
217 $endTime = $endDateTime->format('H:i:s');
218
219 if ($startDate !== $dateString || $endDate !== $dateString) {
220 if (!isset($employeeDays[$startDate])) {
221 $employeeDays[$startDate] = [
222 'groupId' => 'workHours',
223 'periods' => []
224 ];
225 }
226
227 if (!isset($employeeDays[$endDate])) {
228 $employeeDays[$endDate] = [
229 'groupId' => 'workHours',
230 'periods' => []
231 ];
232 }
233
234 $employeeDays[$startDate]['periods'][] = ['groupId' => $groupId, 'start' => $startTime, 'end' => '24:00:00'];
235 $employeeDays[$endDate]['periods'][] = ['groupId' => $groupId, 'start' => '00:00:00', 'end' => $endTime];
236
237 continue;
238 }
239
240 $employeeDays[$dateString]['periods'][] = [
241 'groupId' => $groupId,
242 'start' => $startTime,
243 'end' => $endTime === '00:00:00' ? '24:00:00' : $endTime
244 ];
245 }
246 }
247
248 private function mergeProviderWorkDays(array &$allWorkDays, array $providerWorkDays): void
249 {
250 foreach ($providerWorkDays as $date => $info) {
251 if (!isset($allWorkDays[$date])) {
252 $allWorkDays[$date] = $info;
253
254 continue;
255 }
256
257 if ($allWorkDays[$date]['groupId'] === 'workHours' && $info['groupId'] === 'dayOff') {
258 continue;
259 }
260
261 foreach ($info['periods'] as $period) {
262 $merged = false;
263 foreach ($allWorkDays[$date]['periods'] as &$existingPeriod) {
264 if (
265 $period['groupId'] === $existingPeriod['groupId'] &&
266 $period['start'] <= $existingPeriod['end'] &&
267 $period['end'] >= $existingPeriod['start']
268 ) {
269 $existingPeriod['start'] = min($existingPeriod['start'], $period['start']);
270 $existingPeriod['end'] = max($existingPeriod['end'], $period['end']);
271 $merged = true;
272 break;
273 }
274 }
275
276 if (!$merged) {
277 $allWorkDays[$date]['periods'][] = $period;
278 }
279 }
280 }
281 }
282
283 private function formatWorkDays(array $allWorkDays): array
284 {
285 $formattedPeriods = [];
286
287 foreach ($allWorkDays as $date => $info) {
288 if ($info['groupId'] === 'dayOff') {
289 $formattedPeriods[] = $this->createPeriod($date, $date, 'dayOff', 'day-off');
290 continue;
291 }
292
293 $periods = $info['periods'];
294 if (empty($periods)) {
295 $formattedPeriods[] = $this->createPeriod($date, $date, 'notWorkHours', 'not-work-hours');
296 continue;
297 }
298
299 usort($periods, fn($a, $b) => $a['start'] <=> $b['start']);
300
301 foreach ($periods as $i => $period) {
302 $start = "{$date}T{$period['start']}";
303 $end = "{$date}T{$period['end']}";
304
305 if ($i === 0 && $period['start'] !== '00:00:00') {
306 $formattedPeriods[] = $this->createPeriod("{$date}T00:00:00", $start, 'notWorkHours', 'not-work-hours');
307 }
308
309 $formattedPeriods[] = $this->createPeriod($start, $end, 'workHours', 'work-hours');
310
311 if (isset($periods[$i + 1]) && $period['end'] !== $periods[$i + 1]['start']) {
312 $formattedPeriods[] = $this->createPeriod($end, "{$date}T{$periods[$i + 1]['start']}", 'notWorkHours', 'not-work-hours');
313 }
314
315 if ($i === count($periods) - 1 && $period['end'] !== '24:00:00') {
316 $formattedPeriods[] = $this->createPeriod($end, "{$date}T24:00:00", 'notWorkHours', 'not-work-hours');
317 }
318 }
319 }
320
321 return $formattedPeriods;
322 }
323
324 private function createPeriod(string $start, string $end, string $groupId, string $className): array
325 {
326 return [
327 'groupId' => $groupId,
328 'start' => $start,
329 'end' => $end,
330 'display' => 'background',
331 'className' => $className
332 ];
333 }
334
335 private function getTimeLimitsByProvider(array $queryParams, array $periods, Provider $provider): void
336 {
337 [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = $this->getTimeLimitsFromPeriods(
338 $periods,
339 $this->timeLimits['slotMinTime'],
340 $this->timeLimits['slotMaxTime']
341 );
342
343 if ($this->timeLimits['slotMinTime'] === '24:00:00' && $this->timeLimits['slotMaxTime'] === '00:00:00') {
344 [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] = $this->getLimitsFromCompanyWorkHours();
345 }
346
347 if ($this->timeLimits['slotMinTime'] === '24:00:00' && $this->timeLimits['slotMaxTime'] === '00:00:00') {
348 $this->timeLimits['slotMinTime'] = '09:00:00';
349 $this->timeLimits['slotMaxTime'] = '17:00:00';
350 }
351 }
352
353 private function getTimeLimitsFromPeriods(array $providerWorkDays, string $slotMinTime, string $slotMaxTime): array
354 {
355 foreach ($providerWorkDays as $providerWorkDay) {
356 foreach ($providerWorkDay['periods'] as $period) {
357 $slotMinTime = min($slotMinTime, $period['start']);
358 $slotMaxTime = max($slotMaxTime, $period['end']);
359 }
360 }
361
362 return [$slotMinTime, $slotMaxTime];
363 }
364
365 private function getLimitsFromCompanyWorkHours(): array
366 {
367 $settingsDS = $this->container->get('domain.settings.service');
368 $slotMinTime = '24:00:00';
369 $slotMaxTime = '00:00:00';
370 $companyWorkHours = $settingsDS->getCategorySettings('weekSchedule');
371
372 foreach ($companyWorkHours as $companyWorkHour) {
373 if (!is_null($companyWorkHour['time'][0]) && !is_null($companyWorkHour['time'][1])) {
374 $slotMinTime = min($slotMinTime, $companyWorkHour['time'][0] . ':00');
375 $slotMaxTime = max($slotMaxTime, $companyWorkHour['time'][1] . ':00');
376 }
377 }
378
379 return [$slotMinTime, $slotMaxTime];
380 }
381
382 private function getTimeLimitsFromAppointmentsAndEvents(array $queryParams): void
383 {
384 if (isset($queryParams['entitiesToShow']) && in_array('appointments', $queryParams['entitiesToShow'])) {
385 [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] =
386 $this->getLimitsForAppointments($queryParams, $this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']);
387 }
388
389 if (isset($queryParams['entitiesToShow']) && in_array('events', $queryParams['entitiesToShow'])) {
390 [$this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']] =
391 $this->getLimitsForEvents($queryParams, $this->timeLimits['slotMinTime'], $this->timeLimits['slotMaxTime']);
392 }
393 }
394
395 private function getLimitsForAppointments($queryParams, $slotMinTime, $slotMaxTime): array
396 {
397 $appointmentRepository = $this->container->get('domain.booking.appointment.repository');
398
399 $queryParams['calendarStartDate'] = DateTimeService::getCustomDateTimeInUtc($queryParams['calendarStartDate'] . ' 00:00:00');
400 $queryParams['calendarEndDate'] = DateTimeService::getCustomDateTimeInUtc($queryParams['calendarEndDate'] . ' 23:59:59');
401
402 $statuses = isset($queryParams['statuses']) && in_array('pendingAppointments', $queryParams['statuses'])
403 ? [BookingStatus::APPROVED, BookingStatus::PENDING]
404 : [BookingStatus::APPROVED];
405
406 $appointments = $appointmentRepository->getFiltered([
407 'dates' => [$queryParams['calendarStartDate'], $queryParams['calendarEndDate']],
408 'providers' => !empty($queryParams['providers']) ? $queryParams['providers'] : [],
409 'statuses' => $statuses
410 ]);
411
412 foreach ($appointments->getItems() as $appointment) {
413 $startDateTime = $appointment->getBookingStart()->getValue()->sub(new DateInterval(
414 'PT' . abs($appointment->getService()->getTimeBefore() ? $appointment->getService()->getTimeBefore()->getValue() : 0) . 'S'
415 ));
416 $endDateTime = $appointment->getBookingEnd()->getValue()->add(new DateInterval(
417 'PT' . abs($appointment->getService()->getTimeAfter() ? $appointment->getService()->getTimeAfter()->getValue() : 0) . 'S'
418 ));
419
420 $slotMinTime = min($slotMinTime, $startDateTime->format('H:i:s'));
421 $slotMaxTime = max($slotMaxTime, $endDateTime->format('H:i:s'));
422 }
423
424 return [$slotMinTime, $slotMaxTime];
425 }
426
427 private function getLimitsForEvents($queryParams, $slotMinTime, $slotMaxTime): array
428 {
429 $eventAS = $this->container->get('application.booking.event.service');
430
431 $statuses = isset($queryParams['statuses']) && in_array('pendingAppointments', $queryParams['statuses'])
432 ? [BookingStatus::APPROVED, BookingStatus::PENDING]
433 : [BookingStatus::APPROVED];
434
435 $events = $eventAS->getEventsByCriteria(
436 [
437 'dates' => [$queryParams['calendarStartDate'], $queryParams['calendarEndDate']],
438 'providers' => !empty($queryParams['providers']) ? $queryParams['providers'] : null,
439 'statuses' => $statuses
440 ],
441 ['fetchEventsPeriods' => true],
442 -1
443 );
444
445 foreach ($events->getItems() as $event) {
446 foreach ($event->getPeriods()->getItems() as $period) {
447 $startDateTime = $period->getPeriodStart()->getValue()->format('H:i:s');
448 $endDateTime = $period->getPeriodEnd()->getValue()->format('H:i:s');
449
450 $slotMinTime = min($slotMinTime, $startDateTime);
451 $slotMaxTime = max($slotMaxTime, $endDateTime);
452 }
453 }
454
455 return [$slotMinTime, $slotMaxTime];
456 }
457
458 private function processCompanyDaysOff(array &$allWorkDays, array $queryParams): void
459 {
460 $isDateRangeOverlapping = fn(DateTime $start1, DateTime $end1, DateTime $start2, DateTime $end2): bool =>
461 $start1 <= $end2 && $end1 >= $start2;
462
463 $settingsDS = $this->container->get('domain.settings.service');
464 $calendarStartDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarStartDate']);
465 $calendarEndDate = DateTime::createFromFormat('Y-m-d', $queryParams['calendarEndDate']);
466
467 $companyDaysOff = $settingsDS->getCategorySettings('daysOff');
468
469 foreach ($companyDaysOff as $key => $companyDayOff) {
470 $dayOffStartDate = DateTime::createFromFormat('Y-m-d', $companyDayOff['startDate']);
471 $dayOffEndDate = DateTime::createFromFormat('Y-m-d', $companyDayOff['endDate']);
472
473 if (!$isDateRangeOverlapping($calendarStartDate, $calendarEndDate, $dayOffStartDate, $dayOffEndDate)) {
474 unset($companyDaysOff[$key]);
475 }
476 }
477
478 foreach ($allWorkDays as $date => $info) {
479 $periodDateTime = DateTime::createFromFormat('Y-m-d', $date)->setTime(0, 0);
480
481 foreach ($companyDaysOff as $dayOff) {
482 $dayOffStartDate = DateTime::createFromFormat('Y-m-d', $dayOff['startDate'])->setTime(0, 0);
483 $dayOffEndDate = DateTime::createFromFormat('Y-m-d', $dayOff['endDate'])->setTime(0, 0);
484
485 if ($dayOffStartDate <= $periodDateTime && $dayOffEndDate >= $periodDateTime) {
486 $allWorkDays[$date] = ['groupId' => 'dayOff', 'periods' => []];
487 break;
488 }
489 }
490 }
491 }
492
493 private function convertWorkPeriods($period, string $providerTimezone, DateTimeZone $userTimezone): DateTime
494 {
495 return (new DateTime($period->format('Y-m-d H:i:s'), new DateTimeZone($providerTimezone)))
496 ->setTimezone($userTimezone);
497 }
498
499 private function convertTimeLimits(string $slotMinTime, string $slotMaxTime, Provider $provider): array
500 {
501 $providerTimezone = ($provider->getTimezone() ? $provider->getTimezone()->getValue() : DateTimeService::getTimeZone()->getName());
502 $slotMinTime = $this->applyTimezone($slotMinTime, $providerTimezone);
503 $slotMaxTime = $this->applyTimezone($slotMaxTime, $providerTimezone);
504
505 if ($slotMinTime >= $slotMaxTime) {
506 return ['00:00:00', '24:00:00'];
507 }
508
509 return [$slotMinTime, $slotMaxTime];
510 }
511
512 private function applyTimezone(string $time, string $providerTimezone): string
513 {
514 return (new DateTime($time, new DateTimeZone($providerTimezone)))
515 ->setTimezone(DateTimeService::getTimeZone())
516 ->format('H:i:s');
517 }
518 }
519