19 private const OPTION_NAME =
'isOccupancyCheckerEnabled';
20 private const OPTION_ENABLED =
'Y';
21 private const OPTION_DISABLED =
'N';
23 private const CHECK_LIMIT = 2 * 365 * 86400;
24 private const DISTURBING_EVENTS_LIMIT = 3;
25 private const KEY_PART_START =
'start';
26 private const KEY_PART_END =
'end';
28 private array $timeline = [];
29 private int $checkEventId = 0;
30 private array $cachedEvents = [];
47 if (!$this->canCheck($event))
52 $location = Util::parseLocation($event[
'LOCATION'][
'NEW']);
53 $roomId = $location[
'room_id'];
64 $this->fillTimeline($event, $eventsForCheck, $existingEvents);
65 asort($this->timeline, SORT_NUMERIC);
67 $checkingEventsStartedCount = 0;
68 $existingEventsStartedCount = 0;
69 $disturbingEventDates = [];
70 $isDisturbingEventsAmountOverShowLimit =
false;
71 foreach ($this->timeline as $key => $value)
73 [$eventId, $repeatNumber, $isEnd] = $this->parseTimelineKey($key);
74 if ($eventId === $this->checkEventId)
78 $checkingEventsStartedCount--;
82 $checkingEventsStartedCount++;
89 $existingEventsStartedCount--;
93 $existingEventsStartedCount++;
97 if ($checkingEventsStartedCount > 0 && $existingEventsStartedCount > 0)
99 $disturbingEvent = $this->cachedEvents[$this->getCachedEventKey($eventId, $repeatNumber)];;
100 if (count($disturbingEventDates) >= self::DISTURBING_EVENTS_LIMIT)
102 $isDisturbingEventsAmountOverShowLimit =
true;
105 $disturbingEventDate = $this->getEventFormattedDate($disturbingEvent);
106 if (!in_array($disturbingEventDate, $disturbingEventDates))
108 $disturbingEventDates[] = $disturbingEventDate;
113 if (!empty($disturbingEventDates))
115 $result->addError(
new Error(
'ROOM IS OCCUPIED'));
117 'disturbingEventsFormatted' => implode(
', ', $disturbingEventDates),
118 'isDisturbingEventsAmountOverShowLimit' => $isDisturbingEventsAmountOverShowLimit,
129 private function canCheck(array $event): bool
133 && !empty($event[
'LOCATION'][
'NEW'])
134 && !empty($event[
'DATE_FROM_TS_UTC'])
135 && !empty($event[
'DATE_TO_TS_UTC'])
136 && !empty($event[
'RRULE'])
137 && !empty($event[
'DATE_FROM'])
138 && !empty($event[
'DATE_TO'])
145 private function isEnabled(): bool
147 return Option::get(
'calendar', self::OPTION_NAME, self::OPTION_DISABLED,
'-') === self::OPTION_ENABLED;
157 $toLimit = $this->getCheckLimit($event);
159 \CCalendarEvent::ParseRecursion(
163 'userId' => \CCalendar::GetCurUserId(),
165 'toLimitTs' => $toLimit,
167 'instanceCount' =>
false,
168 'preciseLimits' =>
false,
169 'checkPermission' =>
false,
173 return $checkedEvents;
180 private function getCheckLimit(array $event): int
182 return min($event[
'DATE_FROM_TS_UTC'] + self::CHECK_LIMIT, $event[
'DATE_TO_TS_UTC']);
190 private function getEventTimestamps(array $event): array
192 $dateFrom =
new DateTime($event[
'DATE_FROM']);
193 $dateTo =
new DateTime($event[
'DATE_TO']);
194 $timezoneFrom = $event[
'TZ_FROM'] ??
null;
195 $timezoneTo = $event[
'TZ_TO'] ??
null;
196 $timestampFrom = \Bitrix\Calendar\Util::getDateTimestampUtc($dateFrom, $timezoneFrom);
197 $timestampTo = \Bitrix\Calendar\Util::getDateTimestampUtc($dateTo, $timezoneTo);
198 if (($timestampFrom === $timestampTo) || (($event[
'DT_SKIP_TIME'] ??
null) ===
'Y'))
200 $timestampTo += \CCalendar::GetDayLen();
207 return [$timestampFrom, $timestampTo];
240 $toLimit = $this->getCheckLimit($event);
242 return \CCalendarEvent::GetList(
244 'arSelect' => $arSelect,
246 'SECTION' => [$roomId],
247 'FROM_LIMIT' => $event[
'DATE_FROM'],
252 'parseRecursion' =>
true,
253 'fetchAttendees' =>
false,
254 'fetchMeetings' =>
false,
255 'setDefaultLimit' =>
false,
257 'checkPermissions' =>
false,
258 'parseDescription' =>
false,
259 'fetchSection' =>
false,
260 'getUserfields' =>
false,
271 private function fillTimeline(array $event, array $eventsForCheck, array $existingEvents): void
273 if (!empty($event[
'ID']) && $event[
'ID'] > 0)
275 $this->checkEventId = $event[
'ID'];
279 $previousEventId = 0;
280 foreach ($existingEvents as $existingEvent)
282 if (!$this->canFindIntersections($event, $existingEvent))
287 $currentEventId = (int)$existingEvent[
'PARENT_ID'];
288 if ($currentEventId !== $previousEventId)
290 $previousEventId = $currentEventId;
296 [$timestampFrom, $timestampTo] = $this->getEventTimestamps($existingEvent);
298 $this->timeline[$this->getTimelineKey($currentEventId, $currentIndex,
false)] = $timestampFrom;
299 $this->timeline[$this->getTimelineKey($currentEventId, $currentIndex,
true)] = $timestampTo;
300 $this->cachedEvents[$this->getCachedEventKey($currentEventId, $currentIndex)] = [
301 'ID' => $currentEventId,
302 'DATE_FROM' => $existingEvent[
'DATE_FROM'],
303 'TZ_FROM' => $existingEvent[
'TZ_FROM'],
307 catch (ObjectException $exception)
313 foreach ($eventsForCheck as $eventForCheck)
315 if (empty($eventForCheck[
'DATE_FROM']) || empty($eventForCheck[
'DATE_TO']))
319 [$timestampFrom, $timestampTo] = $this->getEventTimestamps($eventForCheck);
321 $this->timeline[$this->getTimelineKey($this->checkEventId, $currentIndex,
false)] = $timestampFrom;
322 $this->timeline[$this->getTimelineKey($this->checkEventId, $currentIndex,
true)] = $timestampTo;
323 $this->cachedEvents[$this->getCachedEventKey($eventForCheck[
'ID'], $currentIndex)] = [
324 'ID' => $eventForCheck[
'ID'],
325 'DATE_FROM' => $eventForCheck[
'DATE_FROM'],
326 'TZ_FROM' => $eventForCheck[
'TZ_FROM'],
337 private function canFindIntersections(array $event, array $existingEvent): bool
340 ($event[
'PARENT_ID'] ??
null) !== (int)($existingEvent[
'PARENT_ID'] ??
null)
341 && !empty($existingEvent[
'PARENT_ID'])
342 && !empty($existingEvent[
'DATE_FROM'])
343 && !empty($existingEvent[
'DATE_TO'])
344 && !empty($existingEvent[
'ID'])
354 private function getTimelineKey(
int $eventId,
int $repeatNumber,
bool $isEnd): string
356 return $eventId .
'_' . $repeatNumber .
'_' . ($isEnd ? self::KEY_PART_END : self::KEY_PART_START);
363 private function parseTimelineKey(
string $key): array
365 $res = explode(
'_', $key);
366 return [(int)$res[0], (
int)$res[1], ($res[2] === self::KEY_PART_END)];
374 private function getCachedEventKey(
int $eventId,
int $repeatNumber): string
376 return $eventId .
'_' . $repeatNumber;
383 private function getEventFormattedDate(array $event): string
385 $eventTimezone =
null;
386 if (!empty($event[
'TZ_FROM']))
388 $eventTimezone = new \DateTimeZone($event[
'TZ_FROM']);
393 $result = \Bitrix\Calendar\Util::formatEventDate(
394 new DateTime($event[
'DATE_FROM'],
null, $eventTimezone)
397 catch (ObjectException $e)
static createFromTimestamp($timestamp)